diff options
Diffstat (limited to 'src')
101 files changed, 3848 insertions, 13431 deletions
diff --git a/src/account/admin.py b/src/account/admin.py index b4c142c..fd03e60 100644 --- a/src/account/admin.py +++ b/src/account/admin.py @@ -11,9 +11,7 @@ from django.contrib import admin -from account.models import UserProfile, Lab, VlanManager, PublicNetwork +from account.models import UserProfile, Lab admin.site.register(UserProfile) admin.site.register(Lab) -admin.site.register(VlanManager) -admin.site.register(PublicNetwork) diff --git a/src/account/migrations/0010_auto_20230608_1913.py b/src/account/migrations/0010_auto_20230608_1913.py new file mode 100644 index 0000000..3e597b9 --- /dev/null +++ b/src/account/migrations/0010_auto_20230608_1913.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0009_auto_20210324_2107'), + ] + + operations = [ + migrations.RemoveField( + model_name='lab', + name='vlan_manager', + ), + migrations.DeleteModel( + name='PublicNetwork', + ), + migrations.DeleteModel( + name='VlanManager', + ), + ] diff --git a/src/account/models.py b/src/account/models.py index 32229b1..f1deca7 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -16,9 +16,6 @@ import random from collections import Counter -from dashboard.exceptions import ResourceAvailabilityException - - class LabStatus(object): """ A Poor man's enum for the status of a lab. @@ -63,163 +60,6 @@ class UserProfile(models.Model): def __str__(self): return self.user.username - -class VlanManager(models.Model): - """ - Keeps track of the vlans for a lab. - - Vlans are represented as indexes into a 4096 element list. - This list is serialized to JSON for storing in the DB. - """ - - # list of length 4096 containing either 0 (not available) or 1 (available) - vlans = models.TextField() - # list of length 4096 containing either 0 (not reserved) or 1 (reserved) - reserved_vlans = models.TextField() - - block_size = models.IntegerField() - - # True if the lab allows two different users to have the same private vlans - # if they use QinQ or a vxlan overlay, for example - allow_overlapping = models.BooleanField() - - def get_vlans(self, count=1, within=None): - """ - 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. - Always returns a list of ints - - If `within` is not none, will filter against that as a set, requiring that any vlans returned are within that set - """ - allocated = [] - vlans = json.loads(self.vlans) - reserved = json.loads(self.reserved_vlans) - - for i in range(0, len(vlans) - 1): - if len(allocated) >= count: - break - - if vlans[i] == 0 and self.allow_overlapping is False: - continue - - if reserved[i] == 1: - continue - - # vlan is available and not reserved, so safe to add - if within is not None: - if i in within: - allocated.append(i) - else: - allocated.append(i) - continue - - if len(allocated) != count: - raise ResourceAvailabilityException("There were not enough available private vlans for the allocation. Please contact the administrators.") - - return allocated - - def get_public_vlan(self, within=None): - """Return reference to an available public network without reserving it.""" - r = PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False) - if within is not None: - r = r.filter(vlan__in=within) - - if r.count() < 1: - raise ResourceAvailabilityException("There were not enough available public vlans for the allocation. Please contact the administrators.") - - return r.first() - - def reserve_public_vlan(self, vlan): - """Reserves the Public Network that has the given vlan.""" - net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False) - net.in_use = True - net.save() - - def release_public_vlan(self, vlan): - """Un-reserves a public network with the given vlan.""" - net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True) - net.in_use = False - net.save() - - def public_vlan_is_available(self, vlan): - """ - Whether the public vlan is available. - - returns true if the network with the given vlan is free to use, - False otherwise - """ - net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan) - return not net.in_use - - def is_available(self, vlans): - """ - If the vlans are available. - - 'vlans' is either a single vlan id integer or a list of integers - will return true (available) or false - """ - if self.allow_overlapping: - return True - - reserved = json.loads(self.reserved_vlans) - vlan_master_list = json.loads(self.vlans) - try: - iter(vlans) - except Exception: - vlans = [vlans] - - for vlan in vlans: - if not vlan_master_list[vlan] or reserved[vlan]: - return False - return True - - def release_vlans(self, vlans): - """ - Make the vlans available for another booking. - - 'vlans' is either a single vlan id integer or a list of integers - will make the vlans available - doesnt return a value - """ - my_vlans = json.loads(self.vlans) - - try: - iter(vlans) - except Exception: - vlans = [vlans] - - for vlan in vlans: - my_vlans[vlan] = 1 - self.vlans = json.dumps(my_vlans) - self.save() - - def reserve_vlans(self, vlans): - """ - Reserves all given vlans or throws a ValueError. - - vlans can be an integer or a list of integers. - """ - my_vlans = json.loads(self.vlans) - - reserved = json.loads(self.reserved_vlans) - - try: - iter(vlans) - except Exception: - vlans = [vlans] - - vlans = set(vlans) - - for vlan in vlans: - if my_vlans[vlan] == 0 or reserved[vlan] == 1: - raise ValueError("vlan " + str(vlan) + " is not available") - - my_vlans[vlan] = 0 - self.vlans = json.dumps(my_vlans) - self.save() - - class Lab(models.Model): """ Model representing a Hosting Lab. @@ -233,7 +73,6 @@ class Lab(models.Model): contact_email = models.EmailField(max_length=200, null=True, blank=True) contact_phone = models.CharField(max_length=20, null=True, blank=True) status = models.IntegerField(default=LabStatus.UP) - vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True) location = models.TextField(default="unknown") # This token must apear in API requests from this lab api_token = models.CharField(max_length=50) @@ -250,26 +89,9 @@ class Lab(models.Model): key += random.choice(alphabet) return key - def get_available_resources(self): - # Cannot import model normally due to ciruclar import - Server = apps.get_model('resource_inventory', 'Server') # TODO: Find way to import ResourceQuery - resources = [str(resource.profile) for resource in Server.objects.filter(lab=self, working=True, booked=False)] - return dict(Counter(resources)) - def __str__(self): return self.name - -class PublicNetwork(models.Model): - """L2/L3 network that can reach the internet.""" - - vlan = models.IntegerField() - lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - 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): """ A Downtime event. diff --git a/src/account/tests/__init__.py b/src/account/tests/__init__.py deleted file mode 100644 index b6fef6c..0000000 --- a/src/account/tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# 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/src/account/tests/test_general.py b/src/account/tests/test_general.py deleted file mode 100644 index 4020d89..0000000 --- a/src/account/tests/test_general.py +++ /dev/null @@ -1,60 +0,0 @@ -############################################################################## -# 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.auth.models import User -from django.test import Client -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone - -from account.models import UserProfile - - -class AccountMiddlewareTestCase(TestCase): - def setUp(self): - self.client = Client() - self.user1 = User.objects.create(username='user1') - self.user1.set_password('user1') - self.user1profile = UserProfile.objects.create(user=self.user1) - self.user1.save() - - def test_timezone_middleware(self): - """ - Verify timezone is being set by Middleware. - - The timezone should be UTC for anonymous users, - for authenticated users it should be set to user.userprofile.timezone - """ - # default - self.assertEqual(timezone.get_current_timezone_name(), 'UTC') - - url = reverse('account:settings') - # anonymous request - self.client.get(url) - self.assertEqual(timezone.get_current_timezone_name(), 'UTC') - - # authenticated user with UTC timezone (userprofile default) - self.client.login(username='user1', password='user1') - self.client.get(url) - self.assertEqual(timezone.get_current_timezone_name(), 'UTC') - - # authenticated user with custom timezone (userprofile default) - self.user1profile.timezone = 'Etc/Greenwich' - self.user1profile.save() - self.client.get(url) - self.assertEqual(timezone.get_current_timezone_name(), 'GMT') - - # if there is no profile for a user, it should be created - user2 = User.objects.create(username='user2') - user2.set_password('user2') - user2.save() - self.client.login(username='user2', password='user2') - self.client.get(url) - self.assertTrue(user2.userprofile) diff --git a/src/account/urls.py b/src/account/urls.py index 6d4ef2f..23ce122 100644 --- a/src/account/urls.py +++ b/src/account/urls.py @@ -35,11 +35,9 @@ from account.views import ( UserListView, account_resource_view, account_booking_view, - account_images_view, account_detail_view, template_delete_view, booking_cancel_view, - image_delete_view, ) app_name = 'account' @@ -53,7 +51,5 @@ urlpatterns = [ path('my/resources/delete/<int:resource_id>', template_delete_view), url(r'^my/bookings/$', account_booking_view, name='my-bookings'), path('my/bookings/cancel/<int:booking_id>', booking_cancel_view), - url(r'^my/images/$', account_images_view, name='my-images'), - path('my/images/delete/<int:image_id>', image_delete_view), url(r'^my/$', account_detail_view, name='my-account'), ] diff --git a/src/account/views.py b/src/account/views.py index 8976ff9..2d280cd 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -9,6 +9,7 @@ ############################################################################## +import json import os from django.utils import timezone @@ -30,9 +31,7 @@ from mozilla_django_oidc.auth import OIDCAuthenticationBackend from account.forms import AccountSettingsForm from account.models import UserProfile from booking.models import Booking -from resource_inventory.models import ResourceTemplate, Image - - +from api.views import delete_template, liblaas_templates @method_decorator(login_required, name='dispatch') class AccountSettingsView(UpdateView): model = UserProfile @@ -134,17 +133,21 @@ def account_resource_view(request): return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) template = "account/resource_list.html" - active_bundles = [book.resource for book in Booking.objects.filter( - owner=request.user, end__gte=timezone.now(), resource__template__temporary=False)] - active_resources = [bundle.template.id for bundle in active_bundles] - resource_list = list(ResourceTemplate.objects.filter(owner=request.user, temporary=False)) + if request.method == "GET": - context = { - "resources": resource_list, - "active_resources": active_resources, - "title": "My Resources" - } - return render(request, template, context=context) + r = liblaas_templates(request) + usable_templates = r.json() + user_templates = [ t for t in usable_templates if t["owner"] == str(request.user)] + context = { + "templates": user_templates, + "title": "My Resources" + } + return render(request, template, context=context) + + if request.method == "POST": + return delete_template(request) + + return HttpResponse(status_code=405) def account_booking_view(request): @@ -165,37 +168,20 @@ def account_booking_view(request): return render(request, template, context=context) -def account_images_view(request): - if not request.user.is_authenticated: - return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) - template = "account/image_list.html" - my_images = Image.objects.filter(owner=request.user) - public_images = Image.objects.filter(public=True) - used_images = {} - for image in my_images: - if image.in_use(): - used_images[image.id] = "true" - context = { - "title": "Images", - "images": my_images, - "public_images": public_images, - "used_images": used_images - } - return render(request, template, context=context) - def template_delete_view(request, resource_id=None): - if not request.user.is_authenticated: - return HttpResponse(status=403) - template = get_object_or_404(ResourceTemplate, pk=resource_id) - if not request.user.id == template.owner.id: - return HttpResponse(status=403) - if Booking.objects.filter(resource__template=template, end__gt=timezone.now()).exists(): - return HttpResponse(status=403) - template.public = False - template.temporary = True - template.save() - return HttpResponse(status=200) + # if not request.user.is_authenticated: + # return HttpResponse(status=403) + # template = get_object_or_404(ResourceTemplate, pk=resource_id) + # if not request.user.id == template.owner.id: + # return HttpResponse(status=403) + # if Booking.objects.filter(resource__template=template, end__gt=timezone.now()).exists(): + # return HttpResponse(status=403) + # template.public = False + # template.temporary = True + # template.save() + # return HttpResponse(status=200) + return HttpResponse(status=404) # todo - LL Integration def booking_cancel_view(request, booking_id=None): @@ -212,15 +198,3 @@ def booking_cancel_view(request, booking_id=None): booking.save() return HttpResponse('') - -def image_delete_view(request, image_id=None): - if not request.user.is_authenticated: - return HttpResponse('no') # 403? - image = get_object_or_404(Image, pk=image_id) - if image.public or image.owner.id != request.user.id: - return HttpResponse('no') # 403? - # check if used in booking - if image.in_use(): - return HttpResponse('no') # 403? - image.delete() - return HttpResponse('') diff --git a/src/analytics/admin.py b/src/analytics/admin.py index 63f139f..043bad6 100644 --- a/src/analytics/admin.py +++ b/src/analytics/admin.py @@ -6,8 +6,3 @@ # 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/migrations/0003_delete_activevpnuser.py b/src/analytics/migrations/0003_delete_activevpnuser.py new file mode 100644 index 0000000..4d21250 --- /dev/null +++ b/src/analytics/migrations/0003_delete_activevpnuser.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('analytics', '0002_auto_20201109_2149'), + ] + + operations = [ + migrations.DeleteModel( + name='ActiveVPNUser', + ), + ] diff --git a/src/analytics/models.py b/src/analytics/models.py index 10baa0c..043bad6 100644 --- a/src/analytics/models.py +++ b/src/analytics/models.py @@ -6,25 +6,3 @@ # 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/api/admin.py b/src/api/admin.py index 1e243a0..74b023e 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -12,16 +12,6 @@ from django.apps import AppConfig from django.contrib import admin from api.models import ( - Job, - OpnfvApiConfig, - HardwareConfig, - NetworkConfig, - SoftwareConfig, - AccessConfig, - AccessRelation, - SoftwareRelation, - HostHardwareRelation, - HostNetworkRelation, APILog ) @@ -29,15 +19,4 @@ from api.models import ( class ApiConfig(AppConfig): name = 'apiJobs' - -admin.site.register(Job) -admin.site.register(OpnfvApiConfig) -admin.site.register(HardwareConfig) -admin.site.register(NetworkConfig) -admin.site.register(SoftwareConfig) -admin.site.register(AccessConfig) -admin.site.register(AccessRelation) -admin.site.register(SoftwareRelation) -admin.site.register(HostHardwareRelation) -admin.site.register(HostNetworkRelation) admin.site.register(APILog) diff --git a/src/api/migrations/0022_add_cifile_generated_field.py b/src/api/migrations/0022_add_cifile_generated_field.py deleted file mode 100644 index f83a102..0000000 --- a/src/api/migrations/0022_add_cifile_generated_field.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0018_cloudinitfile'), - ] - - operations = [ - migrations.AddField( - model_name="CloudInitFile", - name="generated", - field=models.BooleanField(default=False) - ), - ] diff --git a/src/api/migrations/0022_merge_20211102_2136.py b/src/api/migrations/0022_merge_20230607_1948.py index bb27ae4..2c6fae5 100644 --- a/src/api/migrations/0022_merge_20211102_2136.py +++ b/src/api/migrations/0022_merge_20230607_1948.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2 on 2021-11-02 21:36 +# Generated by Django 2.2 on 2023-06-07 19:48 from django.db import migrations @@ -6,8 +6,8 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('api', '0019_auto_20210907_1448'), ('api', '0021_auto_20210405_1943'), + ('api', '0019_auto_20210907_1448'), ] operations = [ diff --git a/src/api/migrations/0023_auto_20230608_1913.py b/src/api/migrations/0023_auto_20230608_1913.py new file mode 100644 index 0000000..2bc986c --- /dev/null +++ b/src/api/migrations/0023_auto_20230608_1913.py @@ -0,0 +1,172 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0022_merge_20230607_1948'), + ] + + operations = [ + migrations.RemoveField( + model_name='accessrelation', + name='config', + ), + migrations.RemoveField( + model_name='accessrelation', + name='job', + ), + migrations.RemoveField( + model_name='activeusersrelation', + name='config', + ), + migrations.RemoveField( + model_name='activeusersrelation', + name='job', + ), + migrations.RemoveField( + model_name='bridgeconfig', + name='interfaces', + ), + migrations.RemoveField( + model_name='bridgeconfig', + name='opnfv_config', + ), + migrations.RemoveField( + model_name='generatedcloudconfig', + name='booking', + ), + migrations.RemoveField( + model_name='generatedcloudconfig', + name='rconfig', + ), + migrations.RemoveField( + model_name='hardwareconfig', + name='taskconfig_ptr', + ), + migrations.RemoveField( + model_name='hosthardwarerelation', + name='config', + ), + migrations.RemoveField( + model_name='hosthardwarerelation', + name='job', + ), + migrations.RemoveField( + model_name='hostnetworkrelation', + name='config', + ), + migrations.RemoveField( + model_name='hostnetworkrelation', + name='job', + ), + migrations.RemoveField( + model_name='job', + name='booking', + ), + migrations.RemoveField( + model_name='networkconfig', + name='interfaces', + ), + migrations.RemoveField( + model_name='networkconfig', + name='taskconfig_ptr', + ), + migrations.RemoveField( + model_name='opnfvapiconfig', + name='bridge_config', + ), + migrations.RemoveField( + model_name='opnfvapiconfig', + name='opnfv_config', + ), + migrations.RemoveField( + model_name='opnfvapiconfig', + name='roles', + ), + migrations.RemoveField( + model_name='snapshotconfig', + name='taskconfig_ptr', + ), + migrations.RemoveField( + model_name='snapshotrelation', + name='config', + ), + migrations.RemoveField( + model_name='snapshotrelation', + name='job', + ), + migrations.RemoveField( + model_name='snapshotrelation', + name='snapshot', + ), + migrations.RemoveField( + model_name='softwareconfig', + name='opnfv', + ), + migrations.RemoveField( + model_name='softwareconfig', + name='taskconfig_ptr', + ), + migrations.RemoveField( + model_name='softwarerelation', + name='config', + ), + migrations.RemoveField( + model_name='softwarerelation', + name='job', + ), + migrations.DeleteModel( + name='AccessConfig', + ), + migrations.DeleteModel( + name='AccessRelation', + ), + migrations.DeleteModel( + name='ActiveUsersConfig', + ), + migrations.DeleteModel( + name='ActiveUsersRelation', + ), + migrations.DeleteModel( + name='BridgeConfig', + ), + migrations.DeleteModel( + name='GeneratedCloudConfig', + ), + migrations.DeleteModel( + name='HardwareConfig', + ), + migrations.DeleteModel( + name='HostHardwareRelation', + ), + migrations.DeleteModel( + name='HostNetworkRelation', + ), + migrations.DeleteModel( + name='Job', + ), + migrations.DeleteModel( + name='NetworkConfig', + ), + migrations.DeleteModel( + name='OpnfvApiConfig', + ), + migrations.DeleteModel( + name='SnapshotConfig', + ), + migrations.DeleteModel( + name='SnapshotRelation', + ), + migrations.DeleteModel( + name='SoftwareConfig', + ), + migrations.DeleteModel( + name='SoftwareRelation', + ), + migrations.DeleteModel( + name='TaskConfig', + ), + ] diff --git a/src/api/models.py b/src/api/models.py index 93168f5..ca33ed8 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -23,40 +23,10 @@ import yaml import re from booking.models import Booking -from resource_inventory.models import ( - Lab, - ResourceProfile, - Image, - Opsys, - Interface, - ResourceOPNFVConfig, - RemoteInfo, - OPNFVConfig, - ConfigState, - ResourceQuery, - ResourceConfiguration, - CloudInitFile -) -from resource_inventory.idf_templater import IDFTemplater -from resource_inventory.pdf_templater import PDFTemplater from account.models import Downtime, UserProfile from dashboard.utils import AbstractModelQuery -class JobStatus: - """ - A poor man's enum for a job's status. - - A job is NEW if it has not been started or recognized by the Lab - A job is CURRENT if it has been started by the lab but it is not yet completed - a job is DONE if all the tasks are complete and the booking is ready to use - """ - - NEW = 0 - CURRENT = 100 - DONE = 200 - ERROR = 300 - class LabManagerTracker: @@ -89,18 +59,6 @@ class LabManager: def __init__(self, lab): self.lab = lab - def get_opsyss(self): - return Opsys.objects.filter(from_lab=self.lab) - - def get_images(self): - return Image.objects.filter(from_lab=self.lab) - - def get_image(self, image_id): - return Image.objects.filter(from_lab=self.lab, lab_id=image_id) - - def get_opsys(self, opsys_id): - return Opsys.objects.filter(from_lab=self.lab, lab_id=opsys_id) - def get_downtime(self): return Downtime.objects.filter(start__lt=timezone.now(), end__gt=timezone.now(), lab=self.lab) @@ -135,50 +93,6 @@ class LabManager: ) return self.get_downtime_json() - def update_host_remote_info(self, data, res_id): - resource = ResourceQuery.filter(labid=res_id, lab=self.lab) - if len(resource) != 1: - return HttpResponseNotFound("Could not find single host with id " + str(res_id)) - resource = resource[0] - info = {} - try: - info['address'] = data['address'] - info['mac_address'] = data['mac_address'] - info['password'] = data['password'] - info['user'] = data['user'] - info['type'] = data['type'] - info['versions'] = json.dumps(data['versions']) - except Exception as e: - return {"error": "invalid arguement: " + str(e)} - remote_info = resource.remote_management - if "default" in remote_info.mac_address: - remote_info = RemoteInfo() - remote_info.address = info['address'] - remote_info.mac_address = info['mac_address'] - remote_info.password = info['password'] - remote_info.user = info['user'] - remote_info.type = info['type'] - remote_info.versions = info['versions'] - remote_info.save() - resource.remote_management = remote_info - resource.save() - booking = Booking.objects.get(resource=resource.bundle) - self.update_xdf(booking) - return {"status": "success"} - - def update_xdf(self, booking): - booking.pdf = PDFTemplater.makePDF(booking) - booking.idf = IDFTemplater().makeIDF(booking) - booking.save() - - def get_pdf(self, booking_id): - booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab) - return booking.pdf - - def get_idf(self, booking_id): - booking = get_object_or_404(Booking, pk=booking_id, lab=self.lab) - return booking.idf - def get_profile(self): prof = {} prof['name'] = self.lab.name @@ -214,299 +128,12 @@ class LabManager: return json.dumps(self.format_user(profile)) - def get_inventory(self): - inventory = {} - resources = ResourceQuery.filter(lab=self.lab) - images = Image.objects.filter(from_lab=self.lab) - profiles = ResourceProfile.objects.filter(labs=self.lab) - inventory['resources'] = self.serialize_resources(resources) - inventory['images'] = self.serialize_images(images) - inventory['host_types'] = self.serialize_host_profiles(profiles) - return inventory - - def get_host(self, hostname): - resource = ResourceQuery.filter(labid=hostname, lab=self.lab) - if len(resource) != 1: - return HttpResponseNotFound("Could not find single host with id " + str(hostname)) - resource = resource[0] - return { - "booked": resource.booked, - "working": resource.working, - "type": resource.profile.name - } - - def update_host(self, hostname, data): - resource = ResourceQuery.filter(labid=hostname, lab=self.lab) - if len(resource) != 1: - return HttpResponseNotFound("Could not find single host with id " + str(hostname)) - resource = resource[0] - if "working" in data: - working = data['working'] == "true" - resource.working = working - resource.save() - return self.get_host(hostname) - def get_status(self): return {"status": self.lab.status} def set_status(self, payload): {} - def get_current_jobs(self): - jobs = Job.objects.filter(booking__lab=self.lab) - - return self.serialize_jobs(jobs, status=JobStatus.CURRENT) - - def get_new_jobs(self): - jobs = Job.objects.filter(booking__lab=self.lab) - - return self.serialize_jobs(jobs, status=JobStatus.NEW) - - def get_done_jobs(self): - jobs = Job.objects.filter(booking__lab=self.lab) - - 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() - - def update_job(self, jobid, data): - {} - - def serialize_jobs(self, jobs, status=JobStatus.NEW): - job_ser = [] - for job in jobs: - jsonized_job = job.get_delta(status) - if len(jsonized_job['payload']) < 1: - continue - job_ser.append(jsonized_job) - - return job_ser - - def serialize_resources(self, resources): - # TODO: rewrite for Resource model - host_ser = [] - for res in resources: - r = { - 'interfaces': [], - 'hostname': res.name, - 'host_type': res.profile.name - } - for iface in res.get_interfaces(): - r['interfaces'].append({ - 'mac': iface.mac_address, - 'busaddr': iface.bus_address, - 'name': iface.name, - 'switchport': {"switch_name": iface.switch_name, "port_name": iface.port_name} - }) - return host_ser - - def serialize_images(self, images): - images_ser = [] - for image in images: - images_ser.append( - { - "name": image.name, - "lab_id": image.lab_id, - "dashboard_id": image.id - } - ) - return images_ser - - def serialize_resource_profiles(self, profiles): - profile_ser = [] - for profile in profiles: - p = {} - p['cpu'] = { - "cores": profile.cpuprofile.first().cores, - "arch": profile.cpuprofile.first().architecture, - "cpus": profile.cpuprofile.first().cpus, - } - p['disks'] = [] - for disk in profile.storageprofile.all(): - d = { - "size": disk.size, - "type": disk.media_type, - "name": disk.name - } - p['disks'].append(d) - p['description'] = profile.description - p['interfaces'] = [] - for iface in profile.interfaceprofile.all(): - p['interfaces'].append( - { - "speed": iface.speed, - "name": iface.name - } - ) - - p['ram'] = {"amount": profile.ramprofile.first().amount} - p['name'] = profile.name - profile_ser.append(p) - return profile_ser - - -class GeneratedCloudConfig(models.Model): - resource_id = models.CharField(max_length=200) - booking = models.ForeignKey(Booking, on_delete=models.CASCADE) - rconfig = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE) - text = models.TextField(null=True, blank=True) - - def _normalize_username(self, username: str) -> str: - # TODO: make usernames posix compliant - s = re.sub(r'\W+', '', username) - return s - - def _get_ssh_string(self, username: str) -> str: - user = User.objects.get(username=username) - uprofile = user.userprofile - - ssh_file = uprofile.ssh_public_key - - escaped_file = ssh_file.open().read().decode(encoding="UTF-8").replace("\n", " ") - - return escaped_file - - def _serialize_users(self): - """ - returns the dictionary to be placed behind the `users` field of the toplevel c-i dict - """ - # conserves distro default user - user_array = ["default"] - - users = list(self.booking.collaborators.all()) - users.append(self.booking.owner) - for collaborator in users: - userdict = {} - - # TODO: validate if usernames are valid as linux usernames (and provide an override potentially) - userdict['name'] = self._normalize_username(collaborator.username) - - userdict['groups'] = "sudo" - userdict['sudo'] = "ALL=(ALL) NOPASSWD:ALL" - - userdict['ssh_authorized_keys'] = [self._get_ssh_string(collaborator.username)] - - user_array.append(userdict) - - # user_array.append({ - # "name": "opnfv", - # "passwd": "$6$k54L.vim1cLaEc4$5AyUIrufGlbtVBzuCWOlA1yV6QdD7Gr2MzwIs/WhuYR9ebSfh3Qlb7djkqzjwjxpnSAonK1YOabPP6NxUDccu.", - # "ssh_redirect_user": True, - # "sudo": "ALL=(ALL) NOPASSWD:ALL", - # "groups": "sudo", - # }) - - return user_array - - # TODO: make this configurable - def _serialize_sysinfo(self): - defuser = {} - defuser['name'] = 'opnfv' - defuser['plain_text_passwd'] = 'OPNFV_HOST' - defuser['home'] = '/home/opnfv' - defuser['shell'] = '/bin/bash' - defuser['lock_passwd'] = True - defuser['gecos'] = 'Lab Manager User' - defuser['groups'] = 'sudo' - - return {'default_user': defuser} - - # TODO: make this configurable - def _serialize_runcmds(self): - cmdlist = [] - - # have hosts run dhcp on boot - cmdlist.append(['sudo', 'dhclient', '-r']) - cmdlist.append(['sudo', 'dhclient']) - - return cmdlist - - def _serialize_netconf_v1(self): - # interfaces = {} # map from iface_name => dhcp_config - # vlans = {} # map from vlan_id => dhcp_config - - config_arr = [] - - for interface in self._resource().interfaces.all(): - interface_name = interface.profile.name - interface_mac = interface.mac_address - - iface_dict_entry = { - "type": "physical", - "name": interface_name, - "mac_address": interface_mac, - } - - for vlan in interface.config.all(): - if vlan.tagged: - vlan_dict_entry = {'type': 'vlan'} - vlan_dict_entry['name'] = str(interface_name) + "." + str(vlan.vlan_id) - vlan_dict_entry['vlan_link'] = str(interface_name) - vlan_dict_entry['vlan_id'] = int(vlan.vlan_id) - vlan_dict_entry['mac_address'] = str(interface_mac) - if vlan.public: - vlan_dict_entry["subnets"] = [{"type": "dhcp"}] - config_arr.append(vlan_dict_entry) - if (not vlan.tagged) and vlan.public: - iface_dict_entry["subnets"] = [{"type": "dhcp"}] - - # vlan_dict_entry['mtu'] = # TODO, determine override MTU if needed - - config_arr.append(iface_dict_entry) - - ns_dict = { - 'type': 'nameserver', - 'address': ['10.64.0.1', '8.8.8.8'] - } - - config_arr.append(ns_dict) - - full_dict = {'version': 1, 'config': config_arr} - - return full_dict - - @classmethod - def get(cls, booking_id: int, resource_lab_id: str, file_id: int): - return GeneratedCloudConfig.objects.get(resource_id=resource_lab_id, booking__id=booking_id, file_id=file_id) - - def _resource(self): - return ResourceQuery.get(labid=self.resource_id, lab=self.booking.lab) - - # def _get_facts(self): - # resource = self._resource() - - # hostname = self.rconfig.name - # iface_configs = for_config.interface_configs.all() - - def _to_dict(self): - main_dict = {} - - main_dict['users'] = self._serialize_users() - main_dict['network'] = self._serialize_netconf_v1() - main_dict['hostname'] = self.rconfig.name - - # add first startup commands - main_dict['runcmd'] = self._serialize_runcmds() - - # configure distro default user - main_dict['system_info'] = self._serialize_sysinfo() - - return main_dict - - def serialize(self) -> str: - return yaml.dump(self._to_dict(), width=float("inf")) - - class APILog(models.Model): user = models.ForeignKey(User, on_delete=models.PROTECT) call_time = models.DateTimeField(auto_now=True) @@ -534,7 +161,6 @@ class AutomationAPIManager: sbook['end'] = booking.end sbook['lab'] = AutomationAPIManager.serialize_lab(booking.lab) sbook['purpose'] = booking.purpose - sbook['resourceBundle'] = AutomationAPIManager.serialize_bundle(booking.resource) return sbook @staticmethod @@ -545,909 +171,12 @@ class AutomationAPIManager: return slab @staticmethod - def serialize_bundle(bundle): - sbundle = {} - sbundle['id'] = bundle.pk - sbundle['resources'] = [ - AutomationAPIManager.serialize_server(server) - for server in bundle.get_resources()] - return sbundle - - @staticmethod - def serialize_server(server): - sserver = {} - sserver['id'] = server.pk - sserver['name'] = server.name - return sserver - - @staticmethod - def serialize_resource_profile(profile): - sprofile = {} - sprofile['id'] = profile.pk - sprofile['name'] = profile.name - return sprofile - - @staticmethod - def serialize_template(rec_temp_and_count): - template = rec_temp_and_count[0] - count = rec_temp_and_count[1] - - stemplate = {} - stemplate['id'] = template.pk - stemplate['name'] = template.name - stemplate['count_available'] = count - stemplate['resourceProfiles'] = [ - AutomationAPIManager.serialize_resource_profile(config.profile) - for config in template.getConfigs() - ] - return stemplate - - @staticmethod - def serialize_image(image): - simage = {} - simage['id'] = image.pk - simage['name'] = image.name - return simage - - @staticmethod def serialize_userprofile(up): sup = {} sup['id'] = up.pk sup['username'] = up.user.username return sup - -class Job(models.Model): - """ - A Job to be performed by the Lab. - - The API uses Jobs and Tasks to communicate actions that need to be taken to the Lab - that is hosting a booking. A booking from a user has an associated Job which tells - the lab how to configure the hardware, networking, etc to fulfill the booking - for the user. - 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 = {} - for relation in self.get_tasklist(): - if relation.job_key not in d: - d[relation.job_key] = {} - d[relation.job_key][relation.task_id] = relation.config.to_dict() - - return {"id": self.id, "payload": d} - - def get_tasklist(self, status="all"): - if status != "all": - return JobTaskQuery.filter(job=self, status=status) - return JobTaskQuery.filter(job=self) - - def is_fulfilled(self): - """ - If a job has been completed by the lab. - - This method should return true if all of the job's tasks are done, - and false otherwise - """ - my_tasks = self.get_tasklist() - for task in my_tasks: - if task.status != JobStatus.DONE: - return False - return True - - def get_delta(self, status): - d = {} - for relation in self.get_tasklist(status=status): - if relation.job_key not in d: - d[relation.job_key] = {} - d[relation.job_key][relation.task_id] = relation.config.get_delta() - - return {"id": self.id, "payload": d} - - def to_json(self): - return json.dumps(self.to_dict()) - - -class TaskConfig(models.Model): - state = models.IntegerField(default=ConfigState.NEW) - - keys = set() # TODO: This needs to be an instance variable, not a class variable - delta_keys_list = models.CharField(max_length=200, default="[]") - - @property - def delta_keys(self): - return list(set(json.loads(self.delta_keys_list))) - - @delta_keys.setter - def delta_keys(self, keylist): - self.delta_keys_list = json.dumps(keylist) - - def to_dict(self): - raise NotImplementedError - - def get_delta(self): - raise NotImplementedError - - def format_delta(self, config, token): - delta = {k: config[k] for k in self.delta_keys} - delta['lab_token'] = token - return delta - - def to_json(self): - return json.dumps(self.to_dict()) - - def clear_delta(self): - self.delta_keys = [] - - def set(self, *args): - dkeys = self.delta_keys - for arg in args: - if arg in self.keys: - dkeys.append(arg) - self.delta_keys = dkeys - - -class BridgeConfig(models.Model): - """Displays mapping between jumphost interfaces and bridges.""" - - interfaces = models.ManyToManyField(Interface) - opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.CASCADE) - - def to_dict(self): - d = {} - hid = ResourceQuery.get(interface__pk=self.interfaces.first().pk).labid - d[hid] = {} - for interface in self.interfaces.all(): - d[hid][interface.mac_address] = [] - for vlan in interface.config.all(): - network_role = self.opnfv_model.networks().filter(network=vlan.network) - bridge = IDFTemplater.bridge_names[network_role.name] - br_config = { - "vlan_id": vlan.vlan_id, - "tagged": vlan.tagged, - "bridge": bridge - } - d[hid][interface.mac_address].append(br_config) - return d - - def to_json(self): - 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) - scenario = models.CharField(max_length=300) - roles = models.ManyToManyField(ResourceOPNFVConfig) - # pdf and idf are url endpoints, not the actual file - pdf = models.CharField(max_length=100) - idf = models.CharField(max_length=100) - bridge_config = models.OneToOneField(BridgeConfig, on_delete=models.CASCADE, null=True) - delta = models.TextField() - opnfv_config = models.ForeignKey(OPNFVConfig, null=True, on_delete=models.SET_NULL) - - def to_dict(self): - d = {} - if not self.opnfv_config: - return d - if self.installer: - d['installer'] = self.installer - if self.scenario: - d['scenario'] = self.scenario - if self.pdf: - d['pdf'] = self.pdf - if self.idf: - d['idf'] = self.idf - if self.bridge_config: - d['bridged_interfaces'] = self.bridge_config.to_dict() - - hosts = self.roles.all() - if hosts.exists(): - d['roles'] = [] - for host in hosts: - d['roles'].append({ - host.labid: self.opnfv_config.host_opnfv_config.get( - host_config__pk=host.config.pk - ).role.name - }) - - return d - - def to_json(self): - return json.dumps(self.to_dict()) - - def set_installer(self, installer): - self.installer = installer - d = json.loads(self.delta) - d['installer'] = installer - self.delta = json.dumps(d) - - def set_scenario(self, scenario): - self.scenario = scenario - d = json.loads(self.delta) - d['scenario'] = scenario - self.delta = json.dumps(d) - - def set_xdf(self, booking, update_delta=True): - kwargs = {'lab_name': booking.lab.name, 'booking_id': booking.id} - self.pdf = reverse('get-pdf', kwargs=kwargs) - self.idf = reverse('get-idf', kwargs=kwargs) - if update_delta: - d = json.loads(self.delta) - d['pdf'] = self.pdf - d['idf'] = self.idf - self.delta = json.dumps(d) - - def add_role(self, host): - self.roles.add(host) - d = json.loads(self.delta) - if 'role' not in d: - d['role'] = [] - d['roles'].append({host.labid: host.config.opnfvRole.name}) - self.delta = json.dumps(d) - - def clear_delta(self): - self.delta = '{}' - - def get_delta(self): - return json.loads(self.to_json()) - - -class AccessConfig(TaskConfig): - access_type = models.CharField(max_length=50) - user = models.ForeignKey(User, on_delete=models.CASCADE) - revoke = models.BooleanField(default=False) - context = models.TextField(default="") - delta = models.TextField(default="{}") - - def to_dict(self): - d = {} - d['access_type'] = self.access_type - d['user'] = self.user.id - d['revoke'] = self.revoke - try: - d['context'] = json.loads(self.context) - except Exception: - pass - return d - - def get_delta(self): - d = json.loads(self.to_json()) - d["lab_token"] = self.accessrelation.lab_token - - return d - - def to_json(self): - return json.dumps(self.to_dict()) - - def clear_delta(self): - d = {} - d["lab_token"] = self.accessrelation.lab_token - self.delta = json.dumps(d) - - def set_access_type(self, access_type): - self.access_type = access_type - d = json.loads(self.delta) - d['access_type'] = access_type - self.delta = json.dumps(d) - - def set_user(self, user): - self.user = user - d = json.loads(self.delta) - d['user'] = self.user.id - self.delta = json.dumps(d) - - def set_revoke(self, revoke): - self.revoke = revoke - d = json.loads(self.delta) - d['revoke'] = revoke - self.delta = json.dumps(d) - - def set_context(self, context): - self.context = json.dumps(context) - d = json.loads(self.delta) - d['context'] = context - self.delta = json.dumps(d) - - -class SoftwareConfig(TaskConfig): - """Handles software installations, such as OPNFV or ONAP.""" - - opnfv = models.ForeignKey(OpnfvApiConfig, on_delete=models.CASCADE) - - def to_dict(self): - d = {} - if self.opnfv: - d['opnfv'] = self.opnfv.to_dict() - - d["lab_token"] = self.softwarerelation.lab_token - self.delta = json.dumps(d) - - return d - - def get_delta(self): - d = {} - d['opnfv'] = self.opnfv.get_delta() - d['lab_token'] = self.softwarerelation.lab_token - - return d - - def clear_delta(self): - self.opnfv.clear_delta() - - def to_json(self): - return json.dumps(self.to_dict()) - - -class HardwareConfig(TaskConfig): - """Describes the desired configuration of the hardware.""" - - image = models.CharField(max_length=100, default="defimage") - power = models.CharField(max_length=100, default="off") - hostname = models.CharField(max_length=100, default="hostname") - ipmi_create = models.BooleanField(default=False) - delta = models.TextField() - - keys = set(["id", "image", "power", "hostname", "ipmi_create"]) - - def to_dict(self): - return self.get_delta() - - def get_delta(self): - # TODO: grab the GeneratedCloudConfig urls from self.hosthardwarerelation.get_resource() - return self.format_delta( - self.hosthardwarerelation.get_resource().get_configuration(self.state), - self.hosthardwarerelation.lab_token) - - -class NetworkConfig(TaskConfig): - """Handles network configuration.""" - - interfaces = models.ManyToManyField(Interface) - delta = models.TextField() - - def to_dict(self): - d = {} - hid = self.hostnetworkrelation.resource_id - d[hid] = {} - for interface in self.interfaces.all(): - d[hid][interface.mac_address] = [] - if self.state != ConfigState.CLEAN: - for vlan in interface.config.all(): - # TODO: should this come from the interface? - # e.g. will different interfaces for different resources need different configs? - d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged}) - - return d - - def to_json(self): - return json.dumps(self.to_dict()) - - def get_delta(self): - d = json.loads(self.to_json()) - d['lab_token'] = self.hostnetworkrelation.lab_token - return d - - def clear_delta(self): - self.delta = json.dumps(self.to_dict()) - self.save() - - def add_interface(self, interface): - self.interfaces.add(interface) - d = json.loads(self.delta) - hid = self.hostnetworkrelation.resource_id - if hid not in d: - d[hid] = {} - d[hid][interface.mac_address] = [] - for vlan in interface.config.all(): - d[hid][interface.mac_address].append({"vlan_id": vlan.vlan_id, "tagged": vlan.tagged}) - self.delta = json.dumps(d) - - -class SnapshotConfig(TaskConfig): - - resource_id = models.CharField(max_length=200, default="default_id") - image = models.CharField(max_length=200, null=True) # cobbler ID - dashboard_id = models.IntegerField() - delta = models.TextField(default="{}") - - def to_dict(self): - d = {} - if self.host: - d['host'] = self.host.labid - if self.image: - d['image'] = self.image - d['dashboard_id'] = self.dashboard_id - return d - - def to_json(self): - return json.dumps(self.to_dict()) - - def get_delta(self): - d = json.loads(self.to_json()) - return d - - def clear_delta(self): - self.delta = json.dumps(self.to_dict()) - self.save() - - def set_host(self, host): - self.host = host - d = json.loads(self.delta) - d['host'] = host.labid - self.delta = json.dumps(d) - - def set_image(self, image): - self.image = image - d = json.loads(self.delta) - d['image'] = self.image - self.delta = json.dumps(d) - - def clear_image(self): - self.image = None - d = json.loads(self.delta) - d.pop("image", None) - self.delta = json.dumps(d) - - def set_dashboard_id(self, dash): - self.dashboard_id = dash - d = json.loads(self.delta) - d['dashboard_id'] = self.dashboard_id - self.delta = json.dumps(d) - - def save(self, *args, **kwargs): - if len(ResourceQuery.filter(labid=self.resource_id)) != 1: - raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource") - super().save(*args, **kwargs) - - -def get_task(task_id): - for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]: - try: - ret = taskclass.objects.get(task_id=task_id) - return ret - except taskclass.DoesNotExist: - pass - from django.core.exceptions import ObjectDoesNotExist - raise ObjectDoesNotExist("Could not find matching TaskRelation instance") - - +# Needs to exist for migrations def get_task_uuid(): - return str(uuid.uuid4()) - - -class TaskRelation(models.Model): - """ - Relates a Job to a TaskConfig. - - superclass that relates a Job to tasks anc maintains information - like status and messages from the lab - """ - - status = models.IntegerField(default=JobStatus.NEW) - job = models.ForeignKey(Job, on_delete=models.CASCADE) - config = models.OneToOneField(TaskConfig, on_delete=models.CASCADE) - task_id = models.CharField(default=get_task_uuid, max_length=37) - lab_token = models.CharField(default="null", max_length=50) - message = models.TextField(default="") - - job_key = None - - def delete(self, *args, **kwargs): - self.config.delete() - return super(self.__class__, self).delete(*args, **kwargs) - - def type_str(self): - return "Generic Task" - - class Meta: - abstract = True - - -class AccessRelation(TaskRelation): - config = models.OneToOneField(AccessConfig, on_delete=models.CASCADE) - job_key = "access" - - def type_str(self): - return "Access Task" - - def delete(self, *args, **kwargs): - self.config.delete() - return super(self.__class__, self).delete(*args, **kwargs) - - -class SoftwareRelation(TaskRelation): - config = models.OneToOneField(SoftwareConfig, on_delete=models.CASCADE) - job_key = "software" - - def type_str(self): - return "Software Configuration Task" - - def delete(self, *args, **kwargs): - self.config.delete() - return super(self.__class__, self).delete(*args, **kwargs) - - -class HostHardwareRelation(TaskRelation): - resource_id = models.CharField(max_length=200, default="default_id") - config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE) - job_key = "hardware" - - def type_str(self): - return "Hardware Configuration Task" - - def get_delta(self): - return self.config.to_dict() - - def delete(self, *args, **kwargs): - self.config.delete() - return super(self.__class__, self).delete(*args, **kwargs) - - def save(self, *args, **kwargs): - if len(ResourceQuery.filter(labid=self.resource_id)) != 1: - raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource") - super().save(*args, **kwargs) - - def get_resource(self): - return ResourceQuery.get(labid=self.resource_id) - - -class HostNetworkRelation(TaskRelation): - resource_id = models.CharField(max_length=200, default="default_id") - config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE) - job_key = "network" - - def type_str(self): - return "Network Configuration Task" - - def delete(self, *args, **kwargs): - self.config.delete() - return super(self.__class__, self).delete(*args, **kwargs) - - def save(self, *args, **kwargs): - if len(ResourceQuery.filter(labid=self.resource_id)) != 1: - raise ValidationError("resource_id " + str(self.resource_id) + " does not refer to a single resource") - super().save(*args, **kwargs) - - def get_resource(self): - return ResourceQuery.get(labid=self.resource_id) - - -class SnapshotRelation(TaskRelation): - snapshot = models.ForeignKey(Image, on_delete=models.CASCADE) - config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE) - job_key = "snapshot" - - def type_str(self): - return "Snapshot Task" - - def get_delta(self): - return self.config.to_dict() - - def delete(self, *args, **kwargs): - self.config.delete() - 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.""" - - @classmethod - def reimageHost(cls, new_image, booking, host): - """Modify an existing job to reimage the given host.""" - job = Job.objects.get(booking=booking) - # make hardware task new - hardware_relation = HostHardwareRelation.objects.get(resource_id=host, job=job) - hardware_relation.config.image = new_image.lab_id - hardware_relation.config.save() - hardware_relation.status = JobStatus.NEW - - # re-apply networking after host is reset - net_relation = HostNetworkRelation.objects.get(resource_id=host, job=job) - net_relation.status = JobStatus.NEW - - # re-apply ssh access after host is reset - for relation in AccessRelation.objects.filter(job=job, config__access_type="ssh"): - relation.status = JobStatus.NEW - relation.save() - - hardware_relation.save() - net_relation.save() - - @classmethod - def makeSnapshotTask(cls, image, booking, host): - relation = SnapshotRelation() - job = Job.objects.get(booking=booking) - config = SnapshotConfig.objects.create(dashboard_id=image.id) - - relation.job = job - relation.config = config - relation.config.save() - relation.config = relation.config - relation.snapshot = image - relation.save() - - config.clear_delta() - config.set_host(host) - 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() - job = None - try: - job = Job.objects.get(booking=booking) - except Exception: - job = Job.objects.create(status=JobStatus.NEW, booking=booking) - cls.makeHardwareConfigs( - resources=resources, - job=job - ) - cls.makeNetworkConfigs( - resources=resources, - job=job - ) - cls.makeSoftware( - booking=booking, - job=job - ) - cls.makeGeneratedCloudConfigs( - resources=resources, - job=job - ) - all_users = list(booking.collaborators.all()) - all_users.append(booking.owner) - cls.makeAccessConfig( - users=all_users, - access_type="vpn", - revoke=False, - job=job - ) - for user in all_users: - try: - cls.makeAccessConfig( - users=[user], - access_type="ssh", - revoke=False, - job=job, - context={ - "key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"), - "hosts": [r.labid for r in resources] - } - ) - except Exception: - continue - - @classmethod - def makeGeneratedCloudConfigs(cls, resources=[], job=Job()): - for res in resources: - cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=job.booking, rconfig=res.config) - cif.save() - - cif = CloudInitFile.create(priority=0, text=cif.serialize()) - cif.save() - - res.config.cloud_init_files.add(cif) - res.config.save() - - @classmethod - def makeHardwareConfigs(cls, resources=[], job=Job()): - """ - Create and save HardwareConfig. - - Helper function to create the tasks related to - configuring the hardware - """ - for res in resources: - hardware_config = None - try: - hardware_config = HardwareConfig.objects.get(relation__resource_id=res.labid) - except Exception: - hardware_config = HardwareConfig() - - relation = HostHardwareRelation() - relation.resource_id = res.labid - relation.job = job - relation.config = hardware_config - relation.config.save() - relation.config = relation.config - relation.save() - - hardware_config.set("id", "image", "hostname", "power", "ipmi_create") - hardware_config.save() - - @classmethod - def makeAccessConfig(cls, users, access_type, revoke=False, job=Job(), context=False): - """ - Create and save AccessConfig. - - Helper function to create the tasks related to - configuring the VPN, SSH, etc access for users - """ - for user in users: - relation = AccessRelation() - relation.job = job - config = AccessConfig() - config.access_type = access_type - config.user = user - config.save() - relation.config = config - relation.save() - config.clear_delta() - if context: - config.set_context(context) - config.set_access_type(access_type) - config.set_revoke(revoke) - config.set_user(user) - config.save() - - @classmethod - def makeNetworkConfigs(cls, resources=[], job=Job()): - """ - Create and save NetworkConfig. - - Helper function to create the tasks related to - configuring the networking - """ - for res in resources: - network_config = None - try: - network_config = NetworkConfig.objects.get(relation__host=res) - except Exception: - network_config = NetworkConfig.objects.create() - - relation = HostNetworkRelation() - relation.resource_id = res.labid - relation.job = job - network_config.save() - relation.config = network_config - relation.save() - network_config.clear_delta() - - # TODO: use get_interfaces() on resource - for interface in res.interfaces.all(): - network_config.add_interface(interface) - network_config.save() - - @classmethod - def make_bridge_config(cls, booking): - if len(booking.resource.get_resources()) < 2: - return None - try: - jumphost_config = ResourceOPNFVConfig.objects.filter( - role__name__iexact="jumphost" - ) - jumphost = ResourceQuery.filter( - bundle=booking.resource, - config=jumphost_config.resource_config - )[0] - except Exception: - return None - br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config) - for iface in jumphost.interfaces.all(): - br_config.interfaces.add(iface) - return br_config - - @classmethod - def makeSoftware(cls, booking=None, job=Job()): - """ - Create and save SoftwareConfig. - - Helper function to create the tasks related to - configuring the desired software, e.g. an OPNFV deployment - """ - if not booking.opnfv_config: - return None - - opnfv_api_config = OpnfvApiConfig.objects.create( - opnfv_config=booking.opnfv_config, - installer=booking.opnfv_config.installer.name, - scenario=booking.opnfv_config.scenario.name, - bridge_config=cls.make_bridge_config(booking) - ) - - opnfv_api_config.set_xdf(booking, False) - opnfv_api_config.save() - - for host in booking.resource.get_resources(): - opnfv_api_config.roles.add(host) - software_config = SoftwareConfig.objects.create(opnfv=opnfv_api_config) - software_relation = SoftwareRelation.objects.create(job=job, config=software_config) - return software_relation - - -JOB_TASK_CLASSLIST = [ - HostHardwareRelation, - AccessRelation, - HostNetworkRelation, - SoftwareRelation, - SnapshotRelation, - ActiveUsersRelation -] - - -class JobTaskQuery(AbstractModelQuery): - model_list = JOB_TASK_CLASSLIST + pass
\ No newline at end of file diff --git a/src/api/serializers/__init__.py b/src/api/serializers/__init__.py deleted file mode 100644 index e0408fa..0000000 --- a/src/api/serializers/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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/src/api/serializers/booking_serializer.py b/src/api/serializers/booking_serializer.py deleted file mode 100644 index 993eb22..0000000 --- a/src/api/serializers/booking_serializer.py +++ /dev/null @@ -1,173 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 rest_framework import serializers - -from resource_inventory.models import ( - ResourceConfiguration, - CpuProfile, - DiskProfile, - InterfaceProfile, - RamProfile, - Image, - Interface -) - - -class BookingField(serializers.Field): - - def to_representation(self, booking): - """ - Take in a booking object. - - Returns a dictionary of primitives representing that booking - """ - ser = {} - ser['id'] = booking.id - # main loop to grab relevant info out of booking - host_configs = {} # mapping hostname -> config - networks = {} # mapping vlan id -> network_hosts - for host in booking.resource.hosts.all(): - host_configs[host.name] = ResourceConfiguration.objects.get(host=host.template) - if "jumphost" not in ser and host_configs[host.name].opnfvRole.name.lower() == "jumphost": - ser['jumphost'] = host.name - # host is a Host model - for i in range(len(host.interfaces.all())): - interface = host.interfaces.all()[i] - # interface is an Interface model - for vlan in interface.config.all(): - # vlan is Vlan model - if vlan.id not in networks: - networks[vlan.id] = [] - net_host = {"hostname": host.name, "tagged": vlan.tagged, "interface": i} - networks[vlan.id].append(net_host) - # creates networking object of proper form - networking = [] - for vlanid in networks: - network = {} - network['vlan_id'] = vlanid - network['hosts'] = networks[vlanid] - - ser['networking'] = networking - - # creates hosts object of correct form - hosts = [] - for hostname in host_configs: - host = {"hostname": hostname} - host['deploy_image'] = True # TODO? - image = host_configs[hostname].image - host['image'] = { - "name": image.name, - "lab_id": image.lab_id, - "dashboard_id": image.id - } - hosts.append(host) - - ser['hosts'] = hosts - - return ser - - def to_internal_value(self, data): - """ - Take in a dictionary of primitives, and return a booking object. - - This is not going to be implemented or allowed. - If someone needs to create a booking through the api, - they will send a different booking object - """ - return None - - -class BookingSerializer(serializers.Serializer): - - booking = BookingField() - - -# Host Type stuff, for inventory -class CPUSerializer(serializers.ModelSerializer): - class Meta: - model = CpuProfile - fields = ('cores', 'architecture', 'cpus') - - -class DiskSerializer(serializers.ModelSerializer): - class Meta: - model = DiskProfile - fields = ('size', 'media_type', 'name') - - -class InterfaceProfileSerializer(serializers.ModelSerializer): - class Meta: - model = InterfaceProfile - fields = ('speed', 'name') - - -class RamSerializer(serializers.ModelSerializer): - class Meta: - model = RamProfile - fields = ('amount', 'channels') - - -class HostTypeSerializer(serializers.Serializer): - name = serializers.CharField(max_length=200) - ram = RamSerializer() - interface = InterfaceProfileSerializer() - description = serializers.CharField(max_length=1000) - disks = DiskSerializer() - cpu = CPUSerializer() - - -# the rest of the inventory stuff -class NetworkSerializer(serializers.Serializer): - cidr = serializers.CharField(max_length=200) - gateway = serializers.IPAddressField(max_length=200) - vlan = serializers.IntegerField() - - -class ImageSerializer(serializers.ModelSerializer): - lab_id = serializers.IntegerField() - id = serializers.IntegerField(source="dashboard_id") - name = serializers.CharField(max_length=50) - description = serializers.CharField(max_length=200) - - class Meta: - model = Image - - -class InterfaceField(serializers.Field): - def to_representation(self, interface): - pass - - def to_internal_value(self, data): - """Take in a serialized interface and creates an Interface model.""" - mac = data['mac'] - bus_address = data['busaddr'] - switch_name = data['switchport']['switch_name'] - port_name = data['switchport']['port_name'] - # TODO config?? - return Interface.objects.create( - mac_address=mac, - bus_address=bus_address, - switch_name=switch_name, - port_name=port_name - ) - - -class InventoryHostSerializer(serializers.Serializer): - hostname = serializers.CharField(max_length=100) - host_type = serializers.CharField(max_length=100) - interfaces = InterfaceField() - - -class InventorySerializer(serializers.Serializer): - hosts = InventoryHostSerializer() - networks = NetworkSerializer() - images = ImageSerializer() - host_types = HostTypeSerializer() diff --git a/src/api/serializers/old_serializers.py b/src/api/serializers/old_serializers.py deleted file mode 100644 index 0944881..0000000 --- a/src/api/serializers/old_serializers.py +++ /dev/null @@ -1,21 +0,0 @@ -############################################################################## -# 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 rest_framework import serializers - -from account.models import UserProfile - - -class UserSerializer(serializers.ModelSerializer): - username = serializers.CharField(source='user.username') - - class Meta: - model = UserProfile - fields = ('user', 'username', 'ssh_public_key', 'pgp_public_key', 'email_addr') diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py deleted file mode 100644 index 2435a9f..0000000 --- a/src/api/tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# Copyright (c) 2016 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 -############################################################################## diff --git a/src/api/tests/test_models_unittest.py b/src/api/tests/test_models_unittest.py deleted file mode 100644 index 2dee29b..0000000 --- a/src/api/tests/test_models_unittest.py +++ /dev/null @@ -1,271 +0,0 @@ -# Copyright (c) 2019 Sawyer Bergeron, 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 -############################################################################## - -from api.models import ( - Job, - JobStatus, - JobFactory, - HostNetworkRelation, - HostHardwareRelation, - SoftwareRelation, - AccessConfig, - SnapshotRelation -) - -from resource_inventory.models import ( - OPNFVRole, - HostProfile, - ConfigState, -) - -from django.test import TestCase, Client - -from dashboard.testing_utils import ( - make_host, - make_user, - make_user_profile, - make_lab, - make_installer, - make_image, - make_scenario, - make_os, - make_complete_host_profile, - make_booking, -) - - -class ValidBookingCreatesValidJob(TestCase): - @classmethod - def setUpTestData(cls): - cls.user = make_user(False, username="newtestuser", password="testpassword") - cls.userprofile = make_user_profile(cls.user) - cls.lab = make_lab() - - cls.host_profile = make_complete_host_profile(cls.lab) - cls.scenario = make_scenario() - cls.installer = make_installer([cls.scenario]) - os = make_os([cls.installer]) - cls.image = make_image(cls.lab, 1, cls.user, os, cls.host_profile) - for i in range(30): - make_host(cls.host_profile, cls.lab, name="host" + str(i), labid="host" + str(i)) - cls.client = Client() - - def setUp(self): - self.booking, self.compute_hostnames, self.jump_hostname = self.create_multinode_generic_booking() - - def create_multinode_generic_booking(self): - topology = {} - - compute_hostnames = ["cmp01", "cmp02", "cmp03"] - - host_type = HostProfile.objects.first() - - universal_networks = [ - {"name": "public", "tagged": False, "public": True}, - {"name": "admin", "tagged": True, "public": False}] - compute_networks = [{"name": "private", "tagged": True, "public": False}] - jumphost_networks = [{"name": "external", "tagged": True, "public": True}] - - # generate a bunch of extra networks - for i in range(10): - net = {"tagged": False, "public": False} - net["name"] = "net" + str(i) - universal_networks.append(net) - - jumphost_info = { - "type": host_type, - "role": OPNFVRole.objects.get_or_create(name="Jumphost")[0], - "nets": self.make_networks(host_type, jumphost_networks + universal_networks), - "image": self.image - } - topology["jump"] = jumphost_info - - for hostname in compute_hostnames: - host_info = { - "type": host_type, - "role": OPNFVRole.objects.get_or_create(name="Compute")[0], - "nets": self.make_networks(host_type, compute_networks + universal_networks), - "image": self.image - } - topology[hostname] = host_info - - booking = make_booking( - owner=self.user, - lab=self.lab, - topology=topology, - installer=self.installer, - scenario=self.scenario - ) - - if not booking.resource: - raise Exception("Booking does not have a resource when trying to pass to makeCompleteJob") - return booking, compute_hostnames, "jump" - - def make_networks(self, hostprofile, nets): - """ - Distribute nets accross hostprofile's interfaces. - - returns a 2D array - """ - network_struct = [] - count = hostprofile.interfaceprofile.all().count() - for i in range(count): - network_struct.append([]) - while (nets): - index = len(nets) % count - network_struct[index].append(nets.pop()) - - return network_struct - - ################################################################# - # Complete Job Tests - ################################################################# - - def test_complete_job_makes_access_configs(self): - JobFactory.makeCompleteJob(self.booking) - job = Job.objects.get(booking=self.booking) - self.assertIsNotNone(job) - - access_configs = AccessConfig.objects.filter(accessrelation__job=job) - - vpn_configs = access_configs.filter(access_type="vpn") - ssh_configs = access_configs.filter(access_type="ssh") - - self.assertFalse(AccessConfig.objects.exclude(access_type__in=["vpn", "ssh"]).exists()) - - all_users = list(self.booking.collaborators.all()) - all_users.append(self.booking.owner) - - for user in all_users: - self.assertTrue(vpn_configs.filter(user=user).exists()) - self.assertTrue(ssh_configs.filter(user=user).exists()) - - def test_complete_job_makes_network_configs(self): - JobFactory.makeCompleteJob(self.booking) - job = Job.objects.get(booking=self.booking) - self.assertIsNotNone(job) - - booking_hosts = self.booking.resource.hosts.all() - - netrelations = HostNetworkRelation.objects.filter(job=job) - netconfigs = [r.config for r in netrelations] - - netrelation_hosts = [r.host for r in netrelations] - - for config in netconfigs: - for interface in config.interfaces.all(): - self.assertTrue(interface.host in booking_hosts) - - # if no interfaces are referenced that shouldn't have vlans, - # and no vlans exist outside those accounted for in netconfigs, - # then the api is faithfully representing networks - # as netconfigs reference resource_inventory models directly - - # this test relies on the assumption that - # every interface is configured, whether it does or does not have vlans - # if this is not true, the test fails - - for host in booking_hosts: - self.assertTrue(host in netrelation_hosts) - relation = HostNetworkRelation.objects.filter(job=job).get(host=host) - - # do 2 direction matching that interfaces are one to one - config = relation.config - for interface in config.interfaces.all(): - self.assertTrue(interface in host.interfaces) - for interface in host.interfaces.all(): - self.assertTrue(interface in config.interfaces) - - for host in netrelation_hosts: - self.assertTrue(host in booking_hosts) - - def test_complete_job_makes_hardware_configs(self): - JobFactory.makeCompleteJob(self.booking) - job = Job.objects.get(booking=self.booking) - self.assertIsNotNone(job) - - hardware_relations = HostHardwareRelation.objects.filter(job=job) - - job_hosts = [r.host for r in hardware_relations] - - booking_hosts = self.booking.resource.hosts.all() - - self.assertEqual(len(booking_hosts), len(job_hosts)) - - for relation in hardware_relations: - self.assertTrue(relation.host in booking_hosts) - self.assertEqual(relation.status, JobStatus.NEW) - config = relation.config - host = relation.host - self.assertEqual(config.get_delta()["hostname"], host.template.resource.name) - - def test_complete_job_makes_software_configs(self): - JobFactory.makeCompleteJob(self.booking) - job = Job.objects.get(booking=self.booking) - self.assertIsNotNone(job) - - srelation = SoftwareRelation.objects.filter(job=job).first() - self.assertIsNotNone(srelation) - - sconfig = srelation.config - self.assertIsNotNone(sconfig) - - oconfig = sconfig.opnfv - self.assertIsNotNone(oconfig) - - # not onetoone in models, but first() is safe here based on how ConfigBundle and a matching OPNFVConfig are created - # this should, however, be made explicit - self.assertEqual(oconfig.installer, self.booking.config_bundle.opnfv_config.first().installer.name) - self.assertEqual(oconfig.scenario, self.booking.config_bundle.opnfv_config.first().scenario.name) - - for host in oconfig.roles.all(): - role_name = host.config.host_opnfv_config.first().role.name - if str(role_name).lower() == "jumphost": - self.assertEqual(host.template.resource.name, self.jump_hostname) - elif str(role_name).lower() == "compute": - self.assertTrue(host.template.resource.name in self.compute_hostnames) - else: - self.fail(msg="Host with non-configured role name related to job: " + str(role_name)) - - def test_make_snapshot_task(self): - host = self.booking.resource.hosts.first() - image = make_image(self.lab, -1, None, None, host.profile) - - Job.objects.create(booking=self.booking) - - JobFactory.makeSnapshotTask(image, self.booking, host) - - snap_relation = SnapshotRelation.objects.get(job=self.booking.job) - config = snap_relation.config - self.assertEqual(host.id, config.host.id) - self.assertEqual(config.dashboard_id, image.id) - self.assertEqual(snap_relation.snapshot.id, image.id) - - def test_make_hardware_configs(self): - hosts = self.booking.resource.hosts.all() - job = Job.objects.create(booking=self.booking) - JobFactory.makeHardwareConfigs(hosts=hosts, job=job) - - hardware_relations = HostHardwareRelation.objects.filter(job=job) - - self.assertEqual(hardware_relations.count(), hosts.count()) - - host_set = set([h.id for h in hosts]) - - for relation in hardware_relations: - try: - host_set.remove(relation.host.id) - except KeyError: - self.fail("Hardware Relation/Config not created for host " + str(relation.host)) - # TODO: ConfigState needs to be fixed in factory methods - relation.config.state = ConfigState.NEW - self.assertEqual(relation.config.get_delta()["power"], "on") - self.assertTrue(relation.config.get_delta()["ipmi_create"]) - # TODO: the rest of hwconf attrs - - self.assertEqual(len(host_set), 0) diff --git a/src/api/urls.py b/src/api/urls.py index cbb453c..b009aeb 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -31,62 +31,23 @@ from django.urls import path from api.views import ( lab_profile, lab_status, - lab_inventory, lab_downtime, - specific_job, - specific_task, - new_jobs, - current_jobs, - done_jobs, - update_host_bmc, - lab_host, - get_pdf, - get_idf, lab_users, lab_user, GenerateTokenView, - analytics_job, user_bookings, specific_booking, extend_booking, make_booking, list_labs, all_users, - images_for_template, - available_templates, - resource_ci_metadata, - resource_ci_userdata, - resource_ci_userdata_directory, - all_images, - all_opsyss, - single_image, - single_opsys, - create_ci_file, booking_details, ) urlpatterns = [ - path('labs/<slug:lab_name>/opsys/<slug:opsys_id>', single_opsys), - path('labs/<slug:lab_name>/image/<slug:image_id>', single_image), - path('labs/<slug:lab_name>/opsys', all_opsyss), - path('labs/<slug:lab_name>/image', all_images), 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"), - path('labs/<slug:lab_name>/booking/<int:booking_id>/idf', get_idf, name="get-idf"), - path('labs/<slug:lab_name>/jobs/<int:job_id>', specific_job), - path('labs/<slug:lab_name>/jobs/<int:job_id>/<slug:task_id>', specific_task), - path('labs/<slug:lab_name>/jobs/<int:job_id>/cidata/<slug:resource_id>/user-data', resource_ci_userdata_directory, name="specific-user-data"), - path('labs/<slug:lab_name>/jobs/<int:job_id>/cidata/<slug:resource_id>/meta-data', resource_ci_metadata, name="specific-meta-data"), - path('labs/<slug:lab_name>/jobs/<int:job_id>/cidata/<slug:resource_id>/<int:file_id>/user-data', resource_ci_userdata, name="user-data-dir"), - 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), @@ -96,11 +57,6 @@ urlpatterns = [ path('booking/makeBooking', make_booking), path('booking/<int:booking_id>/details', booking_details), - path('resource_inventory/availableTemplates', available_templates), - path('resource_inventory/<int:template_id>/images', images_for_template), - - path('resource_inventory/cloud/create', create_ci_file), - path('users', all_users), path('labs', list_labs), diff --git a/src/api/views.py b/src/api/views.py index d5966ed..ea36a6d 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -10,6 +10,7 @@ import json import math +import os import traceback import sys from datetime import timedelta @@ -21,28 +22,18 @@ from django.utils import timezone from django.views import View from django.http import HttpResponseNotFound from django.http.response import JsonResponse, HttpResponse +import requests 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 django.db.models import Q +from django.contrib.auth.models import User -from api.serializers.booking_serializer import BookingSerializer -from api.serializers.old_serializers import UserSerializer from api.forms import DowntimeForm from account.models import UserProfile, Lab from booking.models import Booking -from booking.quick_deployer import create_from_API -from api.models import LabManagerTracker, get_task, Job, AutomationAPIManager, APILog, GeneratedCloudConfig -from notifier.manager import NotificationHandler -from analytics.models import ActiveVPNUser -from resource_inventory.models import ( - Image, - Opsys, - CloudInitFile, - ResourceQuery, - ResourceTemplate, -) +from api.models import LabManagerTracker,AutomationAPIManager, APILog import yaml import uuid @@ -61,17 +52,6 @@ the correct thing will happen """ -class BookingViewSet(viewsets.ModelViewSet): - queryset = Booking.objects.all() - serializer_class = BookingSerializer - filter_fields = ('resource', 'id') - - -class UserViewSet(viewsets.ModelViewSet): - queryset = UserProfile.objects.all() - serializer_class = UserSerializer - - @method_decorator(login_required, name='dispatch') class GenerateTokenView(View): def get(self, request, *args, **kwargs): @@ -83,111 +63,6 @@ class GenerateTokenView(View): return redirect('account:settings') -def lab_inventory(request, lab_name=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - return JsonResponse(lab_manager.get_inventory(), safe=False) - - -@csrf_exempt -def lab_host(request, lab_name="", host_id=""): - 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_host(host_id), safe=False) - if request.method == "POST": - return JsonResponse(lab_manager.update_host(host_id, request.POST), safe=False) - -# API extension for Cobbler integration - - -def all_images(request, lab_name=""): - a = [] - for i in Image.objects.all(): - a.append(i.serialize()) - return JsonResponse(a, safe=False) - - -def all_opsyss(request, lab_name=""): - a = [] - for opsys in Opsys.objects.all(): - a.append(opsys.serialize()) - - return JsonResponse(a, safe=False) - - -@csrf_exempt -def single_image(request, lab_name="", image_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - img = lab_manager.get_image(image_id).first() - - if request.method == "GET": - if not img: - return HttpResponse(status=404) - return JsonResponse(img.serialize(), safe=False) - - if request.method == "POST": - # get POST data - data = json.loads(request.body.decode('utf-8')) - if img: - img.update(data) - else: - # append lab name and the ID from the URL - data['from_lab_id'] = lab_name - data['lab_id'] = image_id - - # create and save a new Image object - img = Image.new_from_data(data) - - img.save() - - # indicate success in response - return HttpResponse(status=200) - return HttpResponse(status=405) - - -@csrf_exempt -def single_opsys(request, lab_name="", opsys_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - opsys = lab_manager.get_opsys(opsys_id).first() - - if request.method == "GET": - if not opsys: - return HttpResponse(status=404) - return JsonResponse(opsys.serialize(), safe=False) - - if request.method == "POST": - data = json.loads(request.body.decode('utf-8')) - if opsys: - opsys.update(data) - else: - # only name, available, and obsolete are needed to create an Opsys - # other fields are derived from the URL parameters - data['from_lab_id'] = lab_name - data['lab_id'] = opsys_id - opsys = Opsys.new_from_data(data) - - opsys.save() - return HttpResponse(status=200) - return HttpResponse(status=405) - -# end API extension - - -def get_pdf(request, lab_name="", booking_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - return HttpResponse(lab_manager.get_pdf(booking_id), content_type="text/plain") - - -def get_idf(request, lab_name="", booking_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - return HttpResponse(lab_manager.get_idf(booking_id), content_type="text/plain") - - def lab_status(request, lab_name=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') lab_manager = LabManagerTracker.get(lab_name, lab_token) @@ -208,171 +83,12 @@ def lab_user(request, lab_name="", user_id=-1): return HttpResponse(lab_manager.get_user(user_id), content_type="text/plain") -@csrf_exempt -def update_host_bmc(request, lab_name="", host_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - if request.method == "POST": - # update / create RemoteInfo for host - return JsonResponse( - lab_manager.update_host_remote_info(request.POST, host_id), - safe=False - ) - - def lab_profile(request, lab_name=""): lab_token = request.META.get('HTTP_AUTH_TOKEN') lab_manager = LabManagerTracker.get(lab_name, lab_token) return JsonResponse(lab_manager.get_profile(), safe=False) -@csrf_exempt -def specific_task(request, lab_name="", job_id="", task_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - LabManagerTracker.get(lab_name, lab_token) # Authorize caller, but we dont need the result - - if request.method == "POST": - task = get_task(task_id) - if 'status' in request.POST: - task.status = request.POST.get('status') - if 'message' in request.POST: - task.message = request.POST.get('message') - if 'lab_token' in request.POST: - task.lab_token = request.POST.get('lab_token') - task.save() - NotificationHandler.task_updated(task) - d = {} - d['task'] = task.config.get_delta() - m = {} - m['status'] = task.status - m['job'] = str(task.job) - m['message'] = task.message - d['meta'] = m - return JsonResponse(d, safe=False) - elif request.method == "GET": - return JsonResponse(get_task(task_id).config.get_delta()) - - -@csrf_exempt -def specific_job(request, lab_name="", job_id=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - if request.method == "POST": - return JsonResponse(lab_manager.update_job(job_id, request.POST), safe=False) - return JsonResponse(lab_manager.get_job(job_id), safe=False) - - -@csrf_exempt -def resource_ci_userdata(request, lab_name="", job_id="", resource_id="", file_id=0): - # lab_token = request.META.get('HTTP_AUTH_TOKEN') - # lab_manager = LabManagerTracker.get(lab_name, lab_token) - - # job = lab_manager.get_job(job_id) - Job.objects.get(id=job_id) # verify a valid job was given, even if we don't use it - - cifile = None - try: - cifile = CloudInitFile.objects.get(id=file_id) - except ObjectDoesNotExist: - return HttpResponseNotFound("Could not find a matching resource by id " + str(resource_id)) - - text = cifile.text - - prepended_text = "#cloud-config\n" - # mstrat = CloudInitFile.merge_strategy() - # prepended_text = prepended_text + yaml.dump({"merge_strategy": mstrat}) + "\n" - # print("in cloudinitfile create") - text = prepended_text + text - cloud_dict = { - "datasource": { - "None": { - "metadata": { - "instance-id": str(uuid.uuid4()) - }, - "userdata_raw": text, - }, - }, - "datasource_list": ["None"], - } - - return HttpResponse(yaml.dump(cloud_dict, width=float("inf")), status=200) - - -@csrf_exempt -def resource_ci_metadata(request, lab_name="", job_id="", resource_id="", file_id=0): - return HttpResponse("#cloud-config", status=200) - - -@csrf_exempt -def resource_ci_userdata_directory(request, lab_name="", job_id="", resource_id=""): - # files = [{"id": file.file_id, "priority": file.priority} for file in CloudInitFile.objects.filter(job__id=job_id, resource_id=resource_id).order_by("priority").all()] - resource = ResourceQuery.get(labid=resource_id, lab=Lab.objects.get(name=lab_name)) - files = resource.config.cloud_init_files - files = [{"id": file.id, "priority": file.priority} for file in files.order_by("priority").all()] - - d = {} - - merge_failures = [] - - merger = Merger( - [ - (list, ["append"]), - (dict, ["merge"]), - ], - ["override"], # fallback - ["override"], # if types conflict (shouldn't happen in CI, but handle case) - ) - - for f in resource.config.cloud_init_files.order_by("priority").all(): - try: - other_dict = yaml.safe_load(f.text) - if not (type(d) is dict): - raise Exception("CI file was valid yaml but was not a dict") - - merger.merge(d, other_dict) - except Exception as e: - # if fail to merge, then just skip - print("Failed to merge file in, as it had invalid content:", f.id) - print("File text was:") - print(f.text) - merge_failures.append({f.id: str(e)}) - - if len(merge_failures) > 0: - d['merge_failures'] = merge_failures - - file = CloudInitFile.create(text=yaml.dump(d, width=float("inf")), priority=0) - - return HttpResponse(json.dumps([{"id": file.id, "priority": file.priority}]), status=200) - - -def new_jobs(request, lab_name=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - return JsonResponse(lab_manager.get_new_jobs(), safe=False) - - -def current_jobs(request, lab_name=""): - lab_token = request.META.get('HTTP_AUTH_TOKEN') - lab_manager = LabManagerTracker.get(lab_name, lab_token) - 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') @@ -408,12 +124,6 @@ def delete_lab_downtime(lab_manager): 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) - return JsonResponse(lab_manager.get_done_jobs(), safe=False) - - def auth_and_log(request, endpoint): """ Function to authenticate an API user and log info @@ -471,37 +181,41 @@ Booking API Views def user_bookings(request): - token = auth_and_log(request, 'booking') + # token = auth_and_log(request, 'booking') - if isinstance(token, HttpResponse): - return token + # if isinstance(token, HttpResponse): + # return token - bookings = Booking.objects.filter(owner=token.user, end__gte=timezone.now()) - output = [AutomationAPIManager.serialize_booking(booking) - for booking in bookings] - return JsonResponse(output, safe=False) + # bookings = Booking.objects.filter(owner=token.user, end__gte=timezone.now()) + # output = [AutomationAPIManager.serialize_booking(booking) + # for booking in bookings] + # return JsonResponse(output, safe=False) + # todo - LL Integration + return HttpResponse(status=404) @csrf_exempt def specific_booking(request, booking_id=""): - token = auth_and_log(request, 'booking/{}'.format(booking_id)) + # token = auth_and_log(request, 'booking/{}'.format(booking_id)) - if isinstance(token, HttpResponse): - return token + # if isinstance(token, HttpResponse): + # return token - booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) - if request.method == "GET": - sbooking = AutomationAPIManager.serialize_booking(booking) - return JsonResponse(sbooking, safe=False) + # booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) + # if request.method == "GET": + # sbooking = AutomationAPIManager.serialize_booking(booking) + # return JsonResponse(sbooking, safe=False) - if request.method == "DELETE": + # if request.method == "DELETE": - if booking.end < timezone.now(): - return HttpResponse("Booking already over", status=400) + # if booking.end < timezone.now(): + # return HttpResponse("Booking already over", status=400) - booking.end = timezone.now() - booking.save() - return HttpResponse("Booking successfully cancelled") + # booking.end = timezone.now() + # booking.save() + # return HttpResponse("Booking successfully cancelled") + # todo - LL Integration + return HttpResponse(status=404) @csrf_exempt @@ -531,70 +245,82 @@ def extend_booking(request, booking_id="", days=""): @csrf_exempt def make_booking(request): - token = auth_and_log(request, 'booking/makeBooking') - - if isinstance(token, HttpResponse): - return token - + print("received call to make_booking") + data = json.loads(request.body) + print("incoming data is ", data) + + allowed_users = list(data["allowed_users"]) + allowed_users.append(str(request.user)) + + bookingBlob = { + "template_id": data["template_id"], + "allowed_users": allowed_users, + "global_cifile": data["global_cifile"], + "metadata": { + "booking_id": None, # fill in after creating django object + "owner": str(request.user), + "lab": "UNH_IOL", + "purpose": data["metadata"]["purpose"], + "project": data["metadata"]["project"], + "length": data["metadata"]["length"] + } + } + + print("allowed users are ", bookingBlob["allowed_users"]) try: - booking = create_from_API(request.body, token.user) + booking = Booking.objects.create( + purpose=bookingBlob["metadata"]["purpose"], + project=bookingBlob["metadata"]['project'], + lab=Lab.objects.get(name='UNH_IOL'), + owner=request.user, + start=timezone.now(), + end=timezone.now() + timedelta(days=int(bookingBlob["metadata"]['length'])), + ) + print("successfully created booking object with id ", booking.id) + + # Now add collabs + for c in bookingBlob["allowed_users"]: + if c != bookingBlob["metadata"]["owner"]: # Don't add self as a collab + booking.collaborators.add(User.objects.get(username=c)) + print("successfully added collabs") + + # Now create it in liblaas + bookingBlob["metadata"]["booking_id"] = str(booking.id) + liblaas_endpoint = os.environ.get("LIBLAAS_BASE_URL") + 'booking/create' + liblaas_response = requests.post(liblaas_endpoint, data=json.dumps(bookingBlob), headers={'Content-Type': 'application/json'}) + if liblaas_response.status_code != 200: + print("received non success from liblaas") + return JsonResponse( + data={}, + status=500, + safe=False + ) + aggregateId = json.loads(liblaas_response.content) + print("successfully created aggregate in liblaas") - except Exception: - finalTrace = '' - exc_type, exc_value, exc_traceback = sys.exc_info() - for i in traceback.format_exception(exc_type, exc_value, exc_traceback): - finalTrace += '<br>' + i.strip() - return HttpResponse(finalTrace, status=400) + # Now update the agg_id + booking.aggregateId = aggregateId + booking.save() + print("sucessfully updated aggreagateId in booking object") - sbooking = AutomationAPIManager.serialize_booking(booking) - return JsonResponse(sbooking, safe=False) + return JsonResponse( + data = {"bookingId": booking.id}, + status=200, + safe=False + ) + except Exception as error: + print(error) + return JsonResponse( + data={}, + status=500, + safe=False + ) """ Resource Inventory API Views """ - - -def available_templates(request): - token = auth_and_log(request, 'resource_inventory/availableTemplates') - - if isinstance(token, HttpResponse): - return token - - # get available templates - # mirrors MultipleSelectFilter Widget - avt = [] - for lab in Lab.objects.all(): - for template in ResourceTemplate.objects.filter(Q(owner=token.user) | Q(public=True), lab=lab, temporary=False): - available_resources = lab.get_available_resources() - required_resources = template.get_required_resources() - least_available = 100 - - for resource, count_required in required_resources.items(): - try: - curr_count = math.floor(available_resources[str(resource)] / count_required) - if curr_count < least_available: - least_available = curr_count - except KeyError: - least_available = 0 - - if least_available > 0: - avt.append((template, least_available)) - - savt = [AutomationAPIManager.serialize_template(temp) - for temp in avt] - - return JsonResponse(savt, safe=False) - - -def images_for_template(request, template_id=""): - _ = auth_and_log(request, 'resource_inventory/{}/images'.format(template_id)) - - template = get_object_or_404(ResourceTemplate, pk=template_id) - images = [AutomationAPIManager.serialize_image(config.image) - for config in template.getConfigs()] - return JsonResponse(images, safe=False) - +# todo - LL Integration """ User API Views @@ -613,25 +339,6 @@ def all_users(request): return JsonResponse(users, safe=False) -def create_ci_file(request): - token = auth_and_log(request, 'booking/makeCloudConfig') - - if isinstance(token, HttpResponse): - return token - - try: - cconf = request.body - d = yaml.load(cconf) - if not (type(d) is dict): - raise Exception() - - cconf = CloudInitFile.create(text=cconf, priority=CloudInitFile.objects.count()) - - return JsonResponse({"id": cconf.id}) - except Exception: - return JsonResponse({"error": "Provided config file was not valid yaml or was not a dict at the top level"}) - - """ Lab API Views """ @@ -662,95 +369,188 @@ Booking Details API Views def booking_details(request, booking_id=""): - token = auth_and_log(request, 'booking/{}/details'.format(booking_id)) - - if isinstance(token, HttpResponse): - return token - - booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) + # token = auth_and_log(request, 'booking/{}/details'.format(booking_id)) + + # if isinstance(token, HttpResponse): + # return token + + # booking = get_object_or_404(Booking, pk=booking_id, owner=token.user) + + # # overview + # overview = { + # 'username': GeneratedCloudConfig._normalize_username(None, str(token.user)), + # 'purpose': booking.purpose, + # 'project': booking.project, + # 'start_time': booking.start, + # 'end_time': booking.end, + # 'pod_definitions': booking.resource.template, + # 'lab': booking.lab + # } + + # # deployment progress + # task_list = [] + # for task in booking.job.get_tasklist(): + # task_info = { + # 'name': str(task), + # 'status': 'DONE', + # 'lab_response': 'No response provided (yet)' + # } + # if task.status < 100: + # task_info['status'] = 'PENDING' + # elif task.status < 200: + # task_info['status'] = 'IN PROGRESS' + + # if task.message: + # if task.type_str == "Access Task" and request.user.id != task.config.user.id: + # task_info['lab_response'] = '--secret--' + # else: + # task_info['lab_response'] = str(task.message) + # task_list.append(task_info) + + # # pods + # pod_list = [] + # for host in booking.resource.get_resources(): + # pod_info = { + # 'hostname': host.config.name, + # 'machine': host.name, + # 'role': '', + # 'is_headnode': host.config.is_head_node, + # 'image': host.config.image, + # 'ram': {'amount': str(host.profile.ramprofile.first().amount) + 'G', 'channels': host.profile.ramprofile.first().channels}, + # 'cpu': {'arch': host.profile.cpuprofile.first().architecture, 'cores': host.profile.cpuprofile.first().cores, 'sockets': host.profile.cpuprofile.first().cpus}, + # 'disk': {'size': str(host.profile.storageprofile.first().size) + 'GiB', 'type': host.profile.storageprofile.first().media_type, 'mount_point': host.profile.storageprofile.first().name}, + # 'interfaces': [], + # } + # try: + # pod_info['role'] = host.template.opnfvRole + # except Exception: + # pass + # for intprof in host.profile.interfaceprofile.all(): + # int_info = { + # 'name': intprof.name, + # 'speed': intprof.speed + # } + # pod_info['interfaces'].append(int_info) + # pod_list.append(pod_info) + + # # diagnostic info + # diagnostic_info = { + # 'job_id': booking.job.id, + # 'ci_files': '', + # 'pods': [] + # } + # for host in booking.resource.get_resources(): + # pod = { + # 'host': host.name, + # 'configs': [], + + # } + # for ci_file in host.config.cloud_init_files.all(): + # ci_info = { + # 'id': ci_file.id, + # 'text': ci_file.text + # } + # pod['configs'].append(ci_info) + # diagnostic_info['pods'].append(pod) + + # details = { + # 'overview': overview, + # 'deployment_progress': task_list, + # 'pods': pod_list, + # 'diagnostic_info': diagnostic_info, + # 'pdf': booking.pdf + # } + # return JsonResponse(str(details), safe=False) + # todo - LL Integration + return HttpResponse(status=404) + + +""" Forwards a request to the LibLaaS API from a workflow """ +def liblaas_request(request) -> JsonResponse: + print("handing liblaas request... ", request.method) + print(request.body) + if request.method != 'POST': + return JsonResponse({"error" : "405 Method not allowed"}) + + liblaas_base_url = os.environ.get("LIBLAAS_BASE_URL") + post_data = json.loads(request.body) + print("post data is " + str(post_data)) + http_method = post_data["method"] + liblaas_endpoint = post_data["endpoint"] + payload = post_data["workflow_data"] + # Fill in actual username + liblaas_endpoint = liblaas_endpoint.replace("[username]", str(request.user)) + liblaas_endpoint = liblaas_base_url + liblaas_endpoint + print("processed endpoint is ", liblaas_endpoint) + + if (http_method == "GET"): + response = requests.get(liblaas_endpoint, data=json.dumps(payload)) + elif (http_method == "POST"): + response = requests.post(liblaas_endpoint, data=json.dumps(payload), headers={'Content-Type': 'application/json'}) + elif (http_method == "DELETE"): + response = requests.delete(liblaas_endpoint, data=json.dumps(payload)) + elif (http_method == "PUT"): + response = requests.put(liblaas_endpoint, data=json.dumps(payload)) + else: + return JsonResponse( + data={}, + status=405, + safe=False + ) + try: + return JsonResponse( + data=json.loads(response.content.decode('utf8')), + status=200, + safe=False + ) + except Exception as e: + print("fail") + print(e) + return JsonResponse( + data = {}, + status=500, + safe=False + ) - # overview - overview = { - 'username': GeneratedCloudConfig._normalize_username(None, str(token.user)), - 'purpose': booking.purpose, - 'project': booking.project, - 'start_time': booking.start, - 'end_time': booking.end, - 'pod_definitions': booking.resource.template, - 'lab': booking.lab - } +def liblaas_templates(request): + liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "template/list/" + str(request.user) + print("api call to " + liblaas_url) + return requests.get(liblaas_url) - # deployment progress - task_list = [] - for task in booking.job.get_tasklist(): - task_info = { - 'name': str(task), - 'status': 'DONE', - 'lab_response': 'No response provided (yet)' - } - if task.status < 100: - task_info['status'] = 'PENDING' - elif task.status < 200: - task_info['status'] = 'IN PROGRESS' - - if task.message: - if task.type_str == "Access Task" and request.user.id != task.config.user.id: - task_info['lab_response'] = '--secret--' - else: - task_info['lab_response'] = str(task.message) - task_list.append(task_info) - - # pods - pod_list = [] - for host in booking.resource.get_resources(): - pod_info = { - 'hostname': host.config.name, - 'machine': host.name, - 'role': '', - 'is_headnode': host.config.is_head_node, - 'image': host.config.image, - 'ram': {'amount': str(host.profile.ramprofile.first().amount) + 'G', 'channels': host.profile.ramprofile.first().channels}, - 'cpu': {'arch': host.profile.cpuprofile.first().architecture, 'cores': host.profile.cpuprofile.first().cores, 'sockets': host.profile.cpuprofile.first().cpus}, - 'disk': {'size': str(host.profile.storageprofile.first().size) + 'GiB', 'type': host.profile.storageprofile.first().media_type, 'mount_point': host.profile.storageprofile.first().name}, - 'interfaces': [], - } - try: - pod_info['role'] = host.template.opnfvRole - except Exception: - pass - for intprof in host.profile.interfaceprofile.all(): - int_info = { - 'name': intprof.name, - 'speed': intprof.speed - } - pod_info['interfaces'].append(int_info) - pod_list.append(pod_info) - - # diagnostic info - diagnostic_info = { - 'job_id': booking.job.id, - 'ci_files': '', - 'pods': [] - } - for host in booking.resource.get_resources(): - pod = { - 'host': host.name, - 'configs': [], +def delete_template(request): + endpoint = json.loads(request.body)["endpoint"] + liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + endpoint + print("api call to ", liblaas_url) + try: + response = requests.delete(liblaas_url) + return JsonResponse( + data={}, + status=response.status_code, + safe=False + ) + except: + return JsonResponse( + data={}, + status=500, + safe=False + ) - } - for ci_file in host.config.cloud_init_files.all(): - ci_info = { - 'id': ci_file.id, - 'text': ci_file.text - } - pod['configs'].append(ci_info) - diagnostic_info['pods'].append(pod) - - details = { - 'overview': overview, - 'deployment_progress': task_list, - 'pods': pod_list, - 'diagnostic_info': diagnostic_info, - 'pdf': booking.pdf - } - return JsonResponse(str(details), safe=False) +def get_booking_status(bookingObject): + liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "booking/" + bookingObject.aggregateId + "/status" + print("Getting booking status at: ", liblaas_url) + response = requests.get(liblaas_url) + try: + return json.loads(response.content) + except: + print("failed to get status") + return [] + +def liblaas_end_booking(aggregateId): + liblaas_url = os.environ.get('LIBLAAS_BASE_URL') + "booking/" + str(aggregateId) + "/end" + print("Ending booking at ", liblaas_url) + response = requests.delete(liblaas_url) + try: + return response + except: + print("failed to end booking") + return HttpResponse(status=500)
\ No newline at end of file diff --git a/src/booking/forms.py b/src/booking/forms.py index 9c9b053..c7169bb 100644 --- a/src/booking/forms.py +++ b/src/booking/forms.py @@ -7,117 +7,3 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## import django.forms as forms -from django.forms.widgets import NumberInput - -from workflow.forms import ( - MultipleSelectFilterField, - MultipleSelectFilterWidget) -from account.models import UserProfile -from resource_inventory.models import Image, Installer, Scenario -from workflow.forms import SearchableSelectMultipleField -from booking.lib import get_user_items, get_user_field_opts - - -class QuickBookingForm(forms.Form): - # Django Form class for Express Booking - purpose = forms.CharField(max_length=1000) - project = forms.CharField(max_length=400) - hostname = forms.CharField(required=False, max_length=400) - global_cloud_config = forms.CharField(widget=forms.Textarea, required=False) - - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=False) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=False) - - def __init__(self, data=None, user=None, lab_data=None, *args, **kwargs): - if "default_user" in kwargs: - default_user = kwargs.pop("default_user") - else: - default_user = "you" - self.default_user = default_user - - super(QuickBookingForm, self).__init__(data=data, **kwargs) - - image_help_text = 'Image can be set only for single-node bookings. For multi-node bookings set image through Design a POD.' - self.fields["image"] = forms.ModelChoiceField( - Image.objects.filter(public=True) | Image.objects.filter(owner=user), required=False - ) - - self.fields['image'].widget.attrs.update({ - 'class': 'has-popover', - 'data-content': image_help_text, - 'data-placement': 'bottom', - 'data-container': 'body' - }) - - self.fields['users'] = SearchableSelectMultipleField( - queryset=UserProfile.objects.filter(public_user=True).select_related('user').exclude(user=user), - items=get_user_items(exclude=user), - required=False, - **get_user_field_opts() - ) - - self.fields['length'] = forms.IntegerField( - widget=NumberInput( - attrs={ - "type": "range", - 'min': "1", - "max": "21", - "value": "1" - } - ) - ) - - self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**lab_data)) - - hostname_help_text = 'Hostname can be set only for single-node bookings. For multi-node bookings set hostname through Design a POD.' - self.fields['hostname'].widget.attrs.update({ - 'class': 'has-popover', - 'data-content': hostname_help_text, - 'data-placement': 'top', - 'data-container': 'body' - }) - - def build_user_list(self): - """ - Build list of UserProfiles. - - returns a mapping of UserProfile ids to displayable objects expected by - searchable multiple select widget - """ - try: - users = {} - d_qset = UserProfile.objects.select_related('user').all().exclude(user__username=self.default_user) - for userprofile in d_qset: - user = { - 'id': userprofile.user.id, - 'expanded_name': userprofile.full_name, - 'small_name': userprofile.user.username, - 'string': userprofile.email_addr - } - - users[userprofile.user.id] = user - - return users - except Exception: - pass - - def build_search_widget_attrs(self, chosen_users, default_user="you"): - - attrs = { - 'set': self.build_user_list(), - 'show_from_noentry': "false", - 'show_x_results': 10, - 'scrollable': "false", - 'selectable_limit': -1, - 'name': "users", - 'placeholder': "username", - 'initial': chosen_users, - 'edit': False - } - return attrs - - -class HostReImageForm(forms.Form): - - image_id = forms.IntegerField() - host_id = forms.IntegerField() diff --git a/src/booking/migrations/0010_auto_20230608_1913.py b/src/booking/migrations/0010_auto_20230608_1913.py new file mode 100644 index 0000000..66bf63b --- /dev/null +++ b/src/booking/migrations/0010_auto_20230608_1913.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0009_booking_complete'), + ] + + operations = [ + migrations.RemoveField( + model_name='booking', + name='jira_issue_id', + ), + migrations.RemoveField( + model_name='booking', + name='jira_issue_status', + ), + migrations.RemoveField( + model_name='booking', + name='opnfv_config', + ), + migrations.RemoveField( + model_name='booking', + name='resource', + ), + ] diff --git a/src/booking/migrations/0011_booking_aggregateid.py b/src/booking/migrations/0011_booking_aggregateid.py new file mode 100644 index 0000000..111b36b --- /dev/null +++ b/src/booking/migrations/0011_booking_aggregateid.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2 on 2023-07-17 15:25 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0010_auto_20230608_1913'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='aggregateId', + field=models.CharField(blank=True, max_length=36, validators=[django.core.validators.RegexValidator(code='nomatch', message='aggregate_id must be a valid UUID', regex='^[0-9a-fA-F]{8}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{4}\x08-[0-9a-fA-F]{12}$')]), + ), + ] diff --git a/src/booking/models.py b/src/booking/models.py index 966f1c2..09244d3 100644 --- a/src/booking/models.py +++ b/src/booking/models.py @@ -9,11 +9,10 @@ ############################################################################## -from resource_inventory.models import ResourceBundle, OPNFVConfig from account.models import Lab from django.contrib.auth.models import User from django.db import models -import resource_inventory.resource_manager +from django.core.validators import RegexValidator class Booking(models.Model): @@ -26,47 +25,21 @@ class Booking(models.Model): start = models.DateTimeField() end = models.DateTimeField() reset = models.BooleanField(default=False) - jira_issue_id = models.IntegerField(null=True, blank=True) - jira_issue_status = models.CharField(max_length=50, blank=True) purpose = models.CharField(max_length=300, blank=False) # bookings can be extended a limited number of times ext_count = models.IntegerField(default=2) # the hardware that the user has booked - resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True, blank=True) - opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True, blank=True) project = models.CharField(max_length=100, default="", blank=True, null=True) lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL) pdf = models.TextField(blank=True, default="") idf = models.TextField(blank=True, default="") + # Associated LibLaaS aggregate + aggregateId = models.CharField(blank=True, max_length=36, validators=[RegexValidator(regex='^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$', message='aggregate_id must be a valid UUID', code='nomatch')]) complete = models.BooleanField(default=False) class Meta: db_table = 'booking' - def save(self, *args, **kwargs): - """ - Save the booking if self.user is authorized and there is no overlapping booking. - - Raise PermissionError if the user is not authorized - Raise ValueError if there is an overlapping booking - """ - if self.start >= self.end: - raise ValueError('Start date is after end date') - # conflicts end after booking starts, and start before booking ends - conflicting_dates = Booking.objects.filter(resource=self.resource).exclude(id=self.id) - conflicting_dates = conflicting_dates.filter(end__gt=self.start) - conflicting_dates = conflicting_dates.filter(start__lt=self.end) - if conflicting_dates.count() > 0: - raise ValueError('This booking overlaps with another booking') - return super(Booking, self).save(*args, **kwargs) - - def delete(self, *args, **kwargs): - res = self.resource - self.resource = None - self.save() - resource_inventory.resource_manager.ResourceManager.getInstance().deleteResourceBundle(res) - return super(self.__class__, self).delete(*args, **kwargs) - def __str__(self): return str(self.purpose) + ' from ' + str(self.start) + ' until ' + str(self.end) diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py deleted file mode 100644 index 4b85d76..0000000 --- a/src/booking/quick_deployer.py +++ /dev/null @@ -1,343 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 json -import yaml -from django.db.models import Q -from django.db import transaction -from datetime import timedelta -from django.utils import timezone -from django.core.exceptions import ValidationError -from account.models import Lab, UserProfile - -from resource_inventory.models import ( - ResourceTemplate, - Image, - OPNFVRole, - OPNFVConfig, - ResourceOPNFVConfig, - ResourceConfiguration, - NetworkConnection, - InterfaceConfiguration, - Network, - CloudInitFile, -) -from resource_inventory.resource_manager import ResourceManager -from resource_inventory.pdf_templater import PDFTemplater -from notifier.manager import NotificationHandler -from booking.models import Booking -from dashboard.exceptions import BookingLengthException -from api.models import JobFactory - - -def parse_resource_field(resource_json): - """ - Parse the json from the frontend. - - returns a reference to the selected Lab and ResourceTemplate objects - """ - lab, template = (None, None) - lab_dict = resource_json['lab'] - for lab_info in lab_dict.values(): - if lab_info['selected']: - lab = Lab.objects.get(lab_user__id=lab_info['id']) - - resource_dict = resource_json['resource'] - for resource_info in resource_dict.values(): - if resource_info['selected']: - template = ResourceTemplate.objects.get(pk=resource_info['id']) - - if lab is None: - raise ValidationError("No lab was selected") - if template is None: - raise ValidationError("No Host was selected") - - return lab, template - - -def update_template(old_template, image, hostname, user, global_cloud_config=None): - """ - Duplicate a template to the users account and update configured fields. - - The dashboard presents users with preconfigured resource templates, - but the user might want to make small modifications, e.g hostname and - linux distro. So we copy the public template and create a private version - to the user's profile, and mark it temporary. When the booking ends the - new template is deleted - """ - name = user.username + "'s Copy of '" + old_template.name + "'" - num_copies = ResourceTemplate.objects.filter(name__startswith=name).count() - template = ResourceTemplate.objects.create( - name=name if num_copies == 0 else name + " (" + str(num_copies) + ")", - xml=old_template.xml, - owner=user, - lab=old_template.lab, - description=old_template.description, - public=False, - temporary=True, - private_vlan_pool=old_template.private_vlan_pool, - public_vlan_pool=old_template.public_vlan_pool, - copy_of=old_template - ) - - for old_network in old_template.networks.all(): - Network.objects.create( - name=old_network.name, - bundle=template, - is_public=old_network.is_public - ) - # We are assuming there is only one opnfv config per public resource template - old_opnfv = template.opnfv_config.first() - if old_opnfv: - opnfv_config = OPNFVConfig.objects.create( - installer=old_opnfv.installer, - scenario=old_opnfv.installer, - template=template, - name=old_opnfv.installer, - ) - # I am explicitly leaving opnfv_config.networks empty to avoid - # problems with duplicated / shared networks. In the quick deploy, - # there is never multiple networks anyway. This may have to change in the future - - for old_config in old_template.getConfigs(): - image_to_set = image - if not image: - image_to_set = old_config.image - - config = ResourceConfiguration.objects.create( - profile=old_config.profile, - image=image_to_set, - template=template, - is_head_node=old_config.is_head_node, - name=hostname if len(old_template.getConfigs()) == 1 else old_config.name, - # cloud_init_files=old_config.cloud_init_files.set() - ) - - for file in old_config.cloud_init_files.all(): - config.cloud_init_files.add(file) - - if global_cloud_config: - config.cloud_init_files.add(global_cloud_config) - config.save() - - for old_iface_config in old_config.interface_configs.all(): - iface_config = InterfaceConfiguration.objects.create( - profile=old_iface_config.profile, - resource_config=config - ) - - for old_connection in old_iface_config.connections.all(): - iface_config.connections.add(NetworkConnection.objects.create( - network=template.networks.get(name=old_connection.network.name), - vlan_is_tagged=old_connection.vlan_is_tagged - )) - - for old_res_opnfv in old_config.resource_opnfv_config.all(): - if old_opnfv: - ResourceOPNFVConfig.objects.create( - role=old_opnfv.role, - resource_config=config, - opnfv_config=opnfv_config - ) - return template - - -def generate_opnfvconfig(scenario, installer, template): - return OPNFVConfig.objects.create( - scenario=scenario, - installer=installer, - template=template - ) - - -def generate_hostopnfv(hostconfig, opnfvconfig): - role = None - try: - role = OPNFVRole.objects.get(name="Jumphost") - except Exception: - role = OPNFVRole.objects.create( - name="Jumphost", - description="Single server jumphost role" - ) - return ResourceOPNFVConfig.objects.create( - role=role, - host_config=hostconfig, - opnfv_config=opnfvconfig - ) - - -def generate_resource_bundle(template): - resource_manager = ResourceManager.getInstance() - resource_bundle = resource_manager.instantiateTemplate(template) - return resource_bundle - - -def check_invariants(**kwargs): - # TODO: This should really happen in the BookingForm validation methods - image = kwargs['image'] - lab = kwargs['lab'] - length = kwargs['length'] - # check that image os is compatible with installer - if image: - if image.from_lab != lab: - raise ValidationError("The chosen image is not available at the chosen hosting lab") - # TODO - # if image.host_type != host_profile: - # raise ValidationError("The chosen image is not available for the chosen host type") - if not image.public and image.owner != kwargs['owner']: - raise ValidationError("You are not the owner of the chosen private image") - if length < 1 or length > 21: - raise BookingLengthException("Booking must be between 1 and 21 days long") - - -def create_from_form(form, request): - """ - Parse data from QuickBookingForm to create booking - """ - resource_field = form.cleaned_data['filter_field'] - # users_field = form.cleaned_data['users'] - hostname = 'opnfv_host' if not form.cleaned_data['hostname'] else form.cleaned_data['hostname'] - - global_cloud_config = None if not form.cleaned_data['global_cloud_config'] else form.cleaned_data['global_cloud_config'] - - if global_cloud_config: - form.cleaned_data['global_cloud_config'] = create_ci_file(global_cloud_config) - - # image = form.cleaned_data['image'] - # scenario = form.cleaned_data['scenario'] - # installer = form.cleaned_data['installer'] - - lab, resource_template = parse_resource_field(resource_field) - data = form.cleaned_data - data['hostname'] = hostname - data['lab'] = lab - data['resource_template'] = resource_template - data['owner'] = request.user - - return _create_booking(data) - - -def create_from_API(body, user): - """ - Parse data from Automation API to create booking - """ - booking_info = json.loads(body.decode('utf-8')) - - data = {} - data['purpose'] = booking_info['purpose'] - data['project'] = booking_info['project'] - data['users'] = [UserProfile.objects.get(user__username=username) - for username in booking_info['collaborators']] - data['hostname'] = booking_info['hostname'] - data['length'] = booking_info['length'] - data['installer'] = None - data['scenario'] = None - - data['image'] = Image.objects.get(pk=booking_info['imageLabID']) - - data['resource_template'] = ResourceTemplate.objects.get(pk=booking_info['templateID']) - data['lab'] = data['resource_template'].lab - data['owner'] = user - - if 'global_cloud_config' in data.keys(): - data['global_cloud_config'] = CloudInitFile.objects.get(id=data['global_cloud_config']) - - return _create_booking(data) - - -def create_ci_file(data: str) -> CloudInitFile: - try: - d = yaml.load(data) - if not (type(d) is dict): - raise Exception("CI file was valid yaml but was not a dict") - except Exception: - raise ValidationError("The provided Cloud Config is not valid yaml, please refer to the Cloud Init documentation for expected structure") - print("about to create global cloud config") - config = CloudInitFile.create(text=data, priority=CloudInitFile.objects.count()) - print("made global cloud config") - - return config - - -@transaction.atomic -def _create_booking(data): - check_invariants(**data) - - # check booking privileges - # TODO: use the canonical booking_allowed method because now template might have multiple - # machines - if Booking.objects.filter(owner=data['owner'], end__gt=timezone.now()).count() >= 3 and not data['owner'].userprofile.booking_privledge: - raise PermissionError("You do not have permission to have more than 3 bookings at a time.") - - ResourceManager.getInstance().templateIsReservable(data['resource_template']) - - resource_template = update_template(data['resource_template'], data['image'], data['hostname'], data['owner'], global_cloud_config=data['global_cloud_config']) - - # generate resource bundle - resource_bundle = generate_resource_bundle(resource_template) - - # generate booking - booking = Booking.objects.create( - purpose=data['purpose'], - project=data['project'], - lab=data['lab'], - owner=data['owner'], - start=timezone.now(), - end=timezone.now() + timedelta(days=int(data['length'])), - resource=resource_bundle, - opnfv_config=None - ) - - booking.pdf = PDFTemplater.makePDF(booking) - - for collaborator in data['users']: # list of Users (not UserProfile) - booking.collaborators.add(collaborator.user) - - booking.save() - - # generate job - JobFactory.makeCompleteJob(booking) - NotificationHandler.notify_new_booking(booking) - - return booking - - -def drop_filter(user): - """ - Return a dictionary that contains filters. - - Only certain installlers are supported on certain images, etc - so the image filter indexed at [imageid][installerid] is truthy if - that installer is supported on that image - """ - installer_filter = {} - scenario_filter = {} - - images = Image.objects.filter(Q(public=True) | Q(owner=user)) - image_filter = {} - for image in images: - image_filter[image.id] = { - 'lab': 'lab_' + str(image.from_lab.lab_user.id), - 'architecture': str(image.architecture), - 'name': image.name - } - - resource_filter = {} - templates = ResourceTemplate.objects.filter(Q(public=True) | Q(owner=user)) - for rt in templates: - profiles = [conf.profile for conf in rt.getConfigs()] - resource_filter["resource_" + str(rt.id)] = [str(p.architecture) for p in profiles] - - return { - 'installer_filter': json.dumps(installer_filter), - 'scenario_filter': json.dumps(scenario_filter), - 'image_filter': json.dumps(image_filter), - 'resource_profile_map': json.dumps(resource_filter), - } diff --git a/src/booking/stats.py b/src/booking/stats.py deleted file mode 100644 index 5a59d32..0000000 --- a/src/booking/stats.py +++ /dev/null @@ -1,109 +0,0 @@ -############################################################################## -# Copyright (c) 2020 Parker Berberian, Sawyer Bergeron, 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 -############################################################################## -import os -from booking.models import Booking -from resource_inventory.models import ResourceQuery, ResourceProfile -from datetime import datetime, timedelta -from collections import Counter -import pytz - - -class StatisticsManager(object): - - @staticmethod - def getContinuousBookingTimeSeries(span=28): - """ - Calculate Booking usage data points. - - Gathers all active bookings that fall in interval [(now - span), (now + 1 week)]. - x data points are every 12 hours - y values are the integer number of bookings/users active at time - """ - - anuket_colors = [ - '#6BDAD5', # Turquoise - '#E36386', # Pale Violet Red - '#F5B335', # Sandy Brown - '#007473', # Teal - '#BCE194', # Gainsboro - '#00CE7C', # Sea Green - ] - - lfedge_colors = [ - '#0049B0', - '#B481A5', - '#6CAFE4', - '#D33668', - '#28245A' - ] - - x = [] - y = [] - users = [] - projects = [] - profiles = {str(profile): [] for profile in ResourceProfile.objects.all()} - - now = datetime.now(pytz.utc) - delta = timedelta(days=span) - start = now - delta - end = now + timedelta(weeks=1) - - bookings = Booking.objects.filter( - start__lte=end, - end__gte=start - ).prefetch_related("collaborators") - - # get data - while start <= end: - active_users = 0 - - books = bookings.filter( - start__lte=start, - end__gte=start - ).prefetch_related("collaborators") - - for booking in books: - active_users += booking.collaborators.all().count() + 1 - - x.append(str(start.month) + '-' + str(start.day)) - y.append(books.count()) - - step_profiles = Counter([ - str(config.profile) - for book in books - for config in book.resource.template.getConfigs() - ]) - - for profile in ResourceProfile.objects.all(): - profiles[str(profile)].append(step_profiles[str(profile)]) - users.append(active_users) - - start += timedelta(hours=12) - - in_use = len(ResourceQuery.filter(working=True, booked=True)) - not_in_use = len(ResourceQuery.filter(working=True, booked=False)) - maintenance = len(ResourceQuery.filter(working=False)) - - projects = [x.project for x in bookings] - proj_count = sorted(Counter(projects).items(), key=lambda x: x[1]) - - project_keys = [proj[0] for proj in proj_count[-5:]] - project_keys = ['None' if x is None else x for x in project_keys] - project_counts = [proj[1] for proj in proj_count[-5:]] - - resources = {key: [x, value] for key, value in profiles.items()} - - return { - "resources": resources, - "booking": [x, y], - "user": [x, users], - "utils": [in_use, not_in_use, maintenance], - "projects": [project_keys, project_counts], - "colors": anuket_colors if os.environ.get('TEMPLATE_OVERRIDE_DIR') == 'laas' else lfedge_colors - } diff --git a/src/booking/tests/__init__.py b/src/booking/tests/__init__.py deleted file mode 100644 index b6fef6c..0000000 --- a/src/booking/tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# 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/src/booking/tests/test_models.py b/src/booking/tests/test_models.py deleted file mode 100644 index 37eb655..0000000 --- a/src/booking/tests/test_models.py +++ /dev/null @@ -1,210 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.test import TestCase -from django.utils import timezone - -from booking.models import Booking -from dashboard.testing_utils import make_resource_template, make_user - - -class BookingModelTestCase(TestCase): - """ - Test the Booking model. - - Creates all the scafolding needed and tests the Booking model - """ - - def setUp(self): - """ - Prepare for Booking model tests. - - Creates all the needed models, such as users, resources, and configurations - """ - self.owner = User.objects.create(username='owner') - self.res1 = make_resource_template(name="Test template 1") - self.res2 = make_resource_template(name="Test template 2") - self.user1 = make_user(username='user1') - - def test_start_end(self): - """ - Verify the start and end fields. - - if the start of a booking is greater or equal then the end, - saving should raise a ValueException - """ - start = timezone.now() - end = start - timedelta(weeks=1) - self.assertRaises( - ValueError, - Booking.objects.create, - start=start, - end=end, - resource=self.res1, - owner=self.user1, - ) - end = start - self.assertRaises( - ValueError, - Booking.objects.create, - start=start, - end=end, - resource=self.res1, - owner=self.user1, - ) - - def test_conflicts(self): - """ - Verify conflicting dates are dealt with. - - saving an overlapping booking on the same resource - should raise a ValueException - saving for different resources should succeed - """ - start = timezone.now() - end = start + timedelta(weeks=1) - self.assertTrue( - Booking.objects.create( - start=start, - end=end, - owner=self.user1, - resource=self.res1, - ) - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start, - end=end, - resource=self.res1, - owner=self.user1, - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start + timedelta(days=1), - end=end - timedelta(days=1), - resource=self.res1, - owner=self.user1, - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start - timedelta(days=1), - end=end, - resource=self.res1, - owner=self.user1, - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start - timedelta(days=1), - end=end - timedelta(days=1), - resource=self.res1, - owner=self.user1, - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start, - end=end + timedelta(days=1), - resource=self.res1, - owner=self.user1, - ) - - self.assertRaises( - ValueError, - Booking.objects.create, - start=start + timedelta(days=1), - end=end + timedelta(days=1), - resource=self.res1, - owner=self.user1, - ) - - self.assertTrue( - Booking.objects.create( - start=start - timedelta(days=1), - end=start, - owner=self.user1, - resource=self.res1, - ) - ) - - self.assertTrue( - Booking.objects.create( - start=end, - end=end + timedelta(days=1), - owner=self.user1, - resource=self.res1, - ) - ) - - self.assertTrue( - Booking.objects.create( - start=start - timedelta(days=2), - end=start - timedelta(days=1), - owner=self.user1, - resource=self.res1, - ) - ) - - self.assertTrue( - Booking.objects.create( - start=end + timedelta(days=1), - end=end + timedelta(days=2), - owner=self.user1, - resource=self.res1, - ) - ) - - self.assertTrue( - Booking.objects.create( - start=start, - end=end, - owner=self.user1, - resource=self.res2, - ) - ) - - def test_extensions(self): - """ - Test booking extensions. - - saving a booking with an extended end time is allows to happen twice, - and each extension must be a maximum of one week long - """ - start = timezone.now() - end = start + timedelta(weeks=1) - self.assertTrue( - Booking.objects.create( - start=start, - end=end, - owner=self.user1, - resource=self.res1, - ) - ) - - booking = Booking.objects.all().first() # should be only thing in db - - self.assertEquals(booking.ext_count, 2) - booking.end = booking.end + timedelta(days=3) - try: - booking.save() - except Exception: - self.fail("save() threw an exception") diff --git a/src/booking/tests/test_quick_booking.py b/src/booking/tests/test_quick_booking.py deleted file mode 100644 index f405047..0000000 --- a/src/booking/tests/test_quick_booking.py +++ /dev/null @@ -1,180 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 datetime -import json - -from django.test import TestCase, Client - -from booking.models import Booking -from dashboard.testing_utils import ( - make_user, - make_user_profile, - make_lab, - make_image, - make_os, - make_opnfv_role, - make_public_net, - make_resource_template, - make_server -) - - -class QuickBookingValidFormTestCase(TestCase): - @classmethod - def setUpTestData(cls): - cls.user = make_user(False, username="newtestuser") - cls.user.set_password("testpassword") - cls.user.save() - make_user_profile(cls.user, True) - - cls.lab = make_lab() - - cls.res_template = make_resource_template(owner=cls.user, lab=cls.lab) - cls.res_profile = cls.res_template.getConfigs()[0].profile - os = make_os() - cls.image = make_image(cls.res_profile, lab=cls.lab, owner=cls.user, os=os) - cls.server = make_server(cls.res_profile, cls.lab) - cls.role = make_opnfv_role() - cls.pubnet = make_public_net(10, cls.lab) - - cls.post_data = cls.build_post_data() - cls.client = Client() - - @classmethod - def build_post_data(cls): - return { - 'filter_field': json.dumps({ - "resource": { - "resource_" + str(cls.res_profile.id): { - "selected": True, - "id": cls.res_template.id - } - }, - "lab": { - "lab_" + str(cls.lab.lab_user.id): { - "selected": True, - "id": cls.lab.lab_user.id - } - } - }), - 'purpose': 'my_purpose', - 'project': 'my_project', - 'length': '3', - 'ignore_this': 1, - 'users': '', - 'hostname': 'my_host', - 'image': str(cls.image.id), - } - - def post(self, changed_fields={}): - payload = self.post_data.copy() - payload.update(changed_fields) - response = self.client.post('/booking/quick/', payload) - return response - - def setUp(self): - self.client.login(username="newtestuser", password="testpassword") - - def assertValidBooking(self, booking): - self.assertEqual(booking.owner, self.user) - self.assertEqual(booking.purpose, 'my_purpose') - self.assertEqual(booking.project, 'my_project') - delta = booking.end - booking.start - delta -= datetime.timedelta(days=3) - self.assertLess(delta, datetime.timedelta(minutes=1)) - - resource_bundle = booking.resource - - host = resource_bundle.get_resources()[0] - self.assertEqual(host.profile, self.res_profile) - self.assertEqual(host.name, 'my_host') - - def test_with_too_long_length(self): - response = self.post({'length': '22'}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_negative_length(self): - response = self.post({'length': '-1'}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_invalid_installer(self): - response = self.post({'installer': str(self.installer.id + 100)}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_invalid_scenario(self): - response = self.post({'scenario': str(self.scenario.id + 100)}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_invalid_host_id(self): - response = self.post({'filter_field': json.dumps({ - "resource": { - "resource_" + str(self.res_profile.id + 100): { - "selected": True, - "id": self.res_profile.id + 100 - } - }, - "lab": { - "lab_" + str(self.lab.lab_user.id): { - "selected": True, - "id": self.lab.lab_user.id - } - } - })}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_invalid_lab_id(self): - response = self.post({'filter_field': json.dumps({ - "resource": { - "resource_" + str(self.res_profile.id): { - "selected": True, - "id": self.res_profile.id - } - }, - "lab": { - "lab_" + str(self.lab.lab_user.id + 100): { - "selected": True, - "id": self.lab.lab_user.id + 100 - } - } - })}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_invalid_empty_filter_field(self): - response = self.post({'filter_field': ''}) - - self.assertEqual(response.status_code, 200) - self.assertIsNone(Booking.objects.first()) - - def test_with_garbage_users_field(self): # expected behavior: treat as though field is empty if it has garbage data - response = self.post({'users': ['X�]QP�槰DP�+m���h�U�_�yJA:.rDi��QN|.��C��n�P��F!��D�����5ȅj�9�LV��']}) # output from /dev/urandom - - self.assertEqual(response.status_code, 200) - booking = Booking.objects.first() - self.assertIsNone(booking) - - def test_with_valid_form(self): - response = self.post() - - self.assertEqual(response.status_code, 302) # success should redirect - booking = Booking.objects.first() - self.assertIsNotNone(booking) - self.assertValidBooking(booking) diff --git a/src/booking/tests/test_stats.py b/src/booking/tests/test_stats.py deleted file mode 100644 index 5501355..0000000 --- a/src/booking/tests/test_stats.py +++ /dev/null @@ -1,59 +0,0 @@ -############################################################################# -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 -############################################################################## -import pytz -from datetime import timedelta, datetime - -from django.test import TestCase - -from booking.models import Booking -from booking.stats import StatisticsManager as sm -from dashboard.testing_utils import make_user - - -class StatsTestCases(TestCase): - - def test_no_booking_outside_span(self): - now = datetime.now(pytz.utc) - - bad_date = now + timedelta(days=1200) - Booking.objects.create(start=now, end=bad_date, owner=make_user(username='jj')) - - actual = sm.getContinuousBookingTimeSeries() - dates = actual['booking'][0] - - for date in dates: - self.assertNotEqual(date, bad_date) - - def check_booking_and_user_counts(self): - now = datetime.now(pytz.utc) - - for i in range(20): - Booking.objects.create( - start=now, - end=now + timedelta(weeks=3), - owner=make_user(username='a')) - - for i in range(30): - Booking.objects.create( - start=now + timedelta(days=5), - end=now + timedelta(weeks=3, days=5), - owner=make_user(username='a')) - - for i in range(120): - Booking.objects.create( - start=now + timedelta(weeks=1), - end=now + timedelta(weeks=4), - owner=make_user(username='a')) - - dates = [[now, 20], [now + timedelta(days=5), 30], [now + timedelta(weeks=1), 120]] - actual = sm.getContinuousBookingTimeSeries() - - for date in dates: - self.assertEqual(date[1], actual['booking'][date[0]]) - self.assertEqual(date[1], actual['booking'][date[1]]) diff --git a/src/booking/urls.py b/src/booking/urls.py index 0b60351..9784fc5 100644 --- a/src/booking/urls.py +++ b/src/booking/urls.py @@ -32,10 +32,6 @@ from booking.views import ( BookingDeleteView, bookingDelete, BookingListView, - booking_stats_view, - booking_stats_json, - quick_create, - booking_modify_image ) app_name = 'booking' @@ -45,9 +41,5 @@ urlpatterns = [ url(r'^delete/$', BookingDeleteView.as_view(), name='delete_prefix'), url(r'^delete/(?P<booking_id>[0-9]+)/$', BookingDeleteView.as_view(), name='delete'), url(r'^delete/(?P<booking_id>[0-9]+)/confirm/$', bookingDelete, name='delete_booking'), - url(r'^modify/(?P<booking_id>[0-9]+)/image/$', booking_modify_image, name='modify_booking_image'), url(r'^list/$', BookingListView.as_view(), name='list'), - url(r'^stats/$', booking_stats_view, name='stats'), - url(r'^stats/json$', booking_stats_json, name='stats_json'), - url(r'^quick/$', quick_create, name='quick_create'), ] diff --git a/src/booking/views.py b/src/booking/views.py index 367a18d..25cac43 100644 --- a/src/booking/views.py +++ b/src/booking/views.py @@ -18,63 +18,9 @@ from django.shortcuts import redirect, render 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, Lab +from api.views import get_booking_status from booking.models import Booking -from booking.stats import StatisticsManager -from booking.forms import HostReImageForm -from workflow.forms import FormUtils -from api.models import JobFactory, GeneratedCloudConfig -from workflow.views import login -from booking.forms import QuickBookingForm -from booking.quick_deployer import create_from_form, drop_filter -import traceback - - -def quick_create_clear_fields(request): - request.session['quick_create_forminfo'] = None - - -def quick_create(request): - if not request.user.is_authenticated: - return login(request) - - if request.method == 'GET': - context = {} - attrs = FormUtils.getLabData(user=request.user) - 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': - attrs = FormUtils.getLabData(user=request.user) - form = QuickBookingForm(request.POST, lab_data=attrs, user=request.user) - - context = {} - context['lab_profile_map'] = {} - context['form'] = form - - if form.is_valid(): - try: - booking = create_from_form(form, request) - messages.success(request, "We've processed your request. " - "Check Account->My Bookings for the status of your new booking") - return redirect(reverse('booking:booking_detail', kwargs={'booking_id': booking.id})) - except Exception as e: - print("Error occurred while handling quick deployment:") - traceback.print_exc() - print(str(e)) - messages.error(request, "Whoops, an error occurred: " + str(e)) - context.update(drop_filter(request.user)) - return render(request, 'booking/quick_deploy.html', context) - else: - messages.error(request, "Looks like the form didn't validate. Check that you entered everything correctly") - context['status'] = 'false' - context.update(drop_filter(request.user)) - return render(request, 'booking/quick_deploy.html', context) - class BookingView(TemplateView): template_name = "booking/booking_detail.html" @@ -123,31 +69,6 @@ class BookingListView(TemplateView): return context -class ResourceBookingsJSON(View): - def get(self, request, *args, **kwargs): - resource = get_object_or_404(ResourceBundle, id=self.kwargs['resource_id']) - bookings = resource.booking_set.get_queryset().values( - 'id', - 'start', - 'end', - 'purpose', - 'config_bundle__name' - ) - return JsonResponse({'bookings': list(bookings)}) - - -def build_image_mapping(lab, user): - mapping = {} - for profile in ResourceProfile.objects.filter(labs=lab): - images = Image.objects.filter( - from_lab=lab, - architecture=profile.architecture - ).filter( - Q(public=True) | Q(owner=user) - ) - mapping[profile.name] = [{"name": image.name, "value": image.id} for image in images] - return mapping - def booking_detail_view(request, booking_id): user = None @@ -157,18 +78,17 @@ def booking_detail_view(request, booking_id): return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) booking = get_object_or_404(Booking, id=booking_id) + statuses = get_booking_status(booking) allowed_users = set(list(booking.collaborators.all())) allowed_users.add(booking.owner) if user not in allowed_users: return render(request, "dashboard/login.html", {'title': 'This page is private'}) - + context = { 'title': 'Booking Details', 'booking': booking, - 'pdf': booking.pdf, - 'user_id': user.id, - 'image_mapping': build_image_mapping(booking.lab, user), - 'posix_username': GeneratedCloudConfig._normalize_username(None, user.username) + 'statuses': statuses, + 'collab_string': ', '.join(map(str, booking.collaborators.all())) } return render( @@ -176,36 +96,3 @@ def booking_detail_view(request, booking_id): "booking/booking_detail.html", context ) - - -def booking_modify_image(request, booking_id): - form = HostReImageForm(request.POST) - if form.is_valid(): - booking = Booking.objects.get(id=booking_id) - if request.user != booking.owner: - return HttpResponse("unauthorized") - if timezone.now() > booking.end: - return HttpResponse("unauthorized") - new_image = Image.objects.get(id=form.cleaned_data['image_id']) - host = ResourceQuery.get(id=form.cleaned_data['host_id']) - host.config.image = new_image - host.config.save() - JobFactory.reimageHost(new_image, booking, host) - return HttpResponse(new_image.name) - return HttpResponse("error") - - -def booking_stats_view(request): - return render( - request, - "booking/stats.html", - context={"data": StatisticsManager.getContinuousBookingTimeSeries(), "title": ""} - ) - - -def booking_stats_json(request): - try: - span = int(request.GET.get("days", 14)) - except Exception: - span = 14 - return JsonResponse(StatisticsManager.getContinuousBookingTimeSeries(span), safe=False) diff --git a/src/dashboard/admin_utils.py b/src/dashboard/admin_utils.py deleted file mode 100644 index 75e4f3e..0000000 --- a/src/dashboard/admin_utils.py +++ /dev/null @@ -1,811 +0,0 @@ -############################################################################## -# Copyright (c) 2021 Sawyer Bergeron 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 resource_inventory.models import ( - ResourceTemplate, - Image, - Server, - ResourceBundle, - ResourceProfile, - InterfaceProfile, - PhysicalNetwork, - ResourceConfiguration, - NetworkConnection, - InterfaceConfiguration, - Network, - DiskProfile, - CpuProfile, - RamProfile, - Interface, - CloudInitFile, -) - -import json -import yaml -import sys -import inspect -import pydoc -import csv - -from django.contrib.auth.models import User - -from account.models import ( - Lab, - PublicNetwork -) - -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, date, datetime, timezone - -from booking.models import Booking -from notifier.manager import NotificationHandler -from api.models import JobFactory - -from api.models import JobStatus, Job, GeneratedCloudConfig - - -def print_div(): - """ - Utility function for printing dividers, does nothing directly useful as a utility - """ - print("=" * 68) - - -def book_host(owner_username, host_labid, lab_username, hostname, image_id, template_name, length_days=21, collaborator_usernames=[], purpose="internal", project="LaaS"): - """ - creates a quick booking using the given host - - @owner_username is the simple username for the user who will own the resulting booking. - Do not set this to a lab username! - - @image_id is the django id of the image in question, NOT the labid of the image. - Query Image objects by their public status and compatible host types - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username for iol is `unh_iol`, other labs will be documented here - - @hostname the hostname that the resulting host should have set - - @template_name the name of the (public, or user accessible) template to use for this booking - - @length_days how long the booking should be, no hard limit currently - - @collaborator_usernames a list of usernames for collaborators to the booking - - @purpose what this booking will be used for - - @project what project/group this booking is on behalf of or the owner represents - """ - 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): - """ - Mark a host working/not working so that it is either bookable or hidden in the dashboard. - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username: param of the form `unh_iol` or similar - - @working: bool, whether by the end of execution the host should be considered working or not working - """ - - 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): - """ - Mark a host as booked/unbooked - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username: param of the form `unh_iol` or similar - - @working: bool, whether by the end of execution the host should be considered booked or not booked - """ - - 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() - - -def get_host(host_labid, lab_username): - """ - Returns host filtered by lab and then unique id within lab - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username: param of the form `unh_iol` or similar - """ - 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): - """ - Returns various information on the host queried by the given parameters - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username: param of the form `unh_iol` or similar - """ - 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 - - -class CumulativeData: - use_days = 0 - count_bookings = 0 - count_extensions = 0 - - def __init__(self, file_writer): - self.file_writer = file_writer - - def account(self, booking, usage_days): - self.count_bookings += 1 - self.count_extensions += booking.ext_count - self.use_days += usage_days - - def write_cumulative(self): - self.file_writer.writerow([]) - self.file_writer.writerow([]) - self.file_writer.writerow(['Lab Use Days', 'Count of Bookings', 'Total Extensions Used']) - self.file_writer.writerow([self.use_days, self.count_bookings, (self.count_bookings * 2) - self.count_extensions]) - - -def get_years_booking_data(start_year=None, end_year=None): - """ - Outputs yearly booking information from the past 'start_year' years (default: current year) - until the last day of the end year (default current year) as a csv file. - """ - if start_year is None and end_year is None: - start = datetime.combine(date(datetime.now().year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - end = datetime.combine(date(start.year + 1, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - elif end_year is None: - start = datetime.combine(date(start_year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - end = datetime.combine(date(datetime.now().year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - else: - start = datetime.combine(date(start_year, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - end = datetime.combine(date(end_year + 1, 1, 1), datetime.min.time()).replace(tzinfo=timezone.utc) - - if (start.year == end.year - 1): - file_name = "yearly_booking_data_" + str(start.year) + ".csv" - else: - file_name = "yearly_booking_data_" + str(start.year) + "-" + str(end.year - 1) + ".csv" - - with open(file_name, "w", newline="") as file: - file_writer = csv.writer(file) - cumulative_data = CumulativeData(file_writer) - file_writer.writerow( - [ - 'ID', - 'Project', - 'Purpose', - 'User', - 'Collaborators', - 'Extensions Left', - 'Usage Days', - 'Start', - 'End' - ] - ) - - for booking in Booking.objects.filter(start__gte=start, start__lte=end): - filtered = False - booking_filter = [279] - user_filter = ["ParkerBerberian", "ssmith", "ahassick", "sbergeron", "jhodgdon", "rhodgdon", "aburch", "jspewock"] - user = booking.owner.username if booking.owner.username is not None else "None" - - for b in booking_filter: - if b == booking.id: - filtered = True - - for u in user_filter: - if u == user: - filtered = True - # trims time delta to the the specified year(s) if between years - usage_days = ((end if booking.end > end else booking.end) - (start if booking.start < start else booking.start)).days - collaborators = [] - - for c in booking.collaborators.all(): - collaborators.append(c.username) - - if (not filtered): - cumulative_data.account(booking, usage_days) - file_writer.writerow([ - str(booking.id), - str(booking.project), - str(booking.purpose), - str(booking.owner.username), - ','.join(collaborators), - str(booking.ext_count), - str(usage_days), - str(booking.start), - str(booking.end) - ]) - cumulative_data.write_cumulative() - - -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() - - -def detect_leaked_hosts(labid="unh_iol"): - """ - Use this to try to detect leaked hosts. - These hosts may still be in the process of unprovisioning, - but if they are not (or unprovisioning is frozen) then - these hosts are instead leaked - """ - working_servers = Server.objects.filter(working=True, lab__lab_user__username=labid) - booked = working_servers.filter(booked=True) - filtered = booked - print_div() - print("In use now:") - for booking in Booking.objects.filter(end__gte=timezone.now()): - res_for_booking = booking.resource.get_resources() - print(res_for_booking) - for resource in res_for_booking: - filtered = filtered.exclude(id=resource.id) - print_div() - print("Possibly leaked:") - for host in filtered: - print(host) - print_div() - return filtered - - -def booking_for_host(host_labid: str, lab_username="unh_iol"): - """ - Returns the booking that this server is a part of, if any. - Fails with an exception if no such booking exists - - @host_labid is usually of the form `hpe3` or similar, is the labid of the Server (subtype of Resource) object - - @lab_username: param of the form `unh_iol` or similar - """ - server = Server.objects.get(lab__lab_user__username=lab_username, labid=host_labid) - booking = server.bundle.booking_set.first() - print_div() - print(booking) - print("id:", booking.id) - print("owner:", booking.owner) - print("job (id):", booking.job, "(" + str(booking.job.id) + ")") - print_div() - return booking - - -def force_release_booking(booking_id: int): - """ - Takes a booking id and forces the booking to end whether or not the tasks have - completed normally. - - Use with caution! Hosts may or may not be released depending on other underlying issues - - @booking_id: the id of the Booking object to be released - """ - booking = Booking.objects.get(id=booking_id) - job = booking.job - tasks = job.get_tasklist() - for task in tasks: - task.status = JobStatus.DONE - task.save() - - -def free_leaked_public_vlans(safety_buffer_days=2): - for lab in Lab.objects.all(): - current_booking_set = Booking.objects.filter(end__gte=timezone.now() + timedelta(days=safety_buffer_days)) - - marked_nets = set() - - for booking in current_booking_set: - for network in get_network_metadata(booking.id): - marked_nets.add(network["vlan_id"]) - - for net in PublicNetwork.objects.filter(lab=lab).filter(in_use=True): - if net.vlan not in marked_nets: - lab.vlan_manager.release_public_vlan(net.vlan) - - -def get_network_metadata(booking_id: int): - """ - Takes a booking id and prints all (known) networks that are owned by it. - Returns an object of the form {<network name>: {"vlan_id": int, "netname": str <network name>, "public": bool <whether network is public/routable}} - - @booking_id: the id of the Booking object to be queried - """ - booking = Booking.objects.get(id=booking_id) - bundle = booking.resource - pnets = PhysicalNetwork.objects.filter(bundle=bundle).all() - metadata = {} - for pnet in pnets: - net = pnet.generic_network - mdata = {"vlan_id": pnet.vlan_id, "netname": net.name, "public": net.is_public} - metadata[net.name] = mdata - return metadata - - -def print_dict_pretty(a_dict): - """ - admin_utils internal function - """ - - print(json.dumps(a_dict, sort_keys=True, indent=4)) - - -def import_host(filenames): - """ - Imports host from an array of converted inspection files and if needed creates a new profile for the host. - NOTE: CONVERT INSPECTION FILES USING convert_inspect_results(["file", "file"]) - (original file names not including "-import.yaml" i.e. hpe44) AND FILL IN <NEEDED FIELDS> BEFORE THIS - @filenames: array of host import file names to import - """ - - for filename in filenames: - - # open import file - file = open("dashboard/" + filename + "-import.yaml", "r") - data = yaml.safe_load(file) - - # if a new profile is needed create one and a matching template - if (data["new_profile"]): - add_profile(data) - print("Profile: " + data["name"] + " created!") - make_default_template( - ResourceProfile.objects.get(name=data["name"]), - Image.objects.get(lab_id=data["image"]).id, - None, - None, - False, - False, - data["owner"], - "unh_iol", - True, - False, - data["temp_desc"] - ) - - print(" Template: " + data["temp_name"] + " created!") - - # add the server - add_server( - ResourceProfile.objects.get(name=data["name"]), - data["hostname"], - data["interfaces"], - data["lab"], - data["vendor"], - data["model"] - ) - - print(data["hostname"] + " imported!") - - -def convert_inspect_results(files): - """ - Converts an array of inspection result files into templates (filename-import.yaml) to be filled out for importing the servers into the dashboard - @files an array of file names (not including the file type. i.e hpe44). Default: [] - """ - for filename in files: - # open host inspect file - file = open("dashboard/" + filename + ".yaml") - output = open("dashboard/" + filename + "-import.yaml", "w") - data = json.load(file) - - # gather data about disks - disk_data = {} - for i in data["disk"]: - - # don't include loops in disks - if "loop" not in i: - disk_data[i["name"]] = { - "capacity": i["size"][:-3], - "media_type": "<\"SSD\" or \"HDD\">", - "interface": "<\"sata\", \"sas\", \"ssd\", \"nvme\", \"scsi\", or \"iscsi\">", - } - - # gather interface data - interface_data = {} - for i in data["interfaces"]: - interface_data[data["interfaces"][i]["name"]] = { - "speed": data["interfaces"][i]["speed"], - "nic_type": "<\"onboard\" or \"pcie\">", - "order": "<order in switch>", - "mac_address": data["interfaces"][i]["mac"], - "bus_addr": data["interfaces"][i]["busaddr"], - } - - # gather cpu data - cpu_data = { - "cores": data["cpu"]["cores"], - "architecture": data["cpu"]["arch"], - "cpus": data["cpu"]["cpus"], - "cflags": "<cflags string>", - } - - # gather ram data - ram_data = { - "amount": data["memory"][:-1], - "channels": "<int of ram channels used>", - } - - # assemble data for host import file - import_data = { - "new_profile": "<True or False> (Set to True to create a new profile for the host's type)", - "name": "<profile name> (Used to set the profile of a host and for creating a new profile)", - "description": "<profile description>", - "labs": "<labs using profile>", - "temp_name": "<Template name>", - "temp_desc": "<template description>", - "image": "<image lab_id>", - "owner": "<template owner>", - "hostname": data["hostname"], - "lab": "<lab server is in> (i.e. \"unh_iol\")", - "disks": disk_data, - "interfaces": interface_data, - "cpus": cpu_data, - "ram": ram_data, - "vendor": "<host vendor>", - "model": "<host model>", - } - - # export data as yaml - yaml.dump(import_data, output) - - -def add_profile(data): - """ - Used for adding a host profile to the dashboard - - schema (of dict passed as "data" param): - { - "name": str - "description": str - "labs": [ - str (lab username) - ] - "disks": { - <diskname> : { - capacity: int (GiB) - media_type: str ("SSD" or "HDD") - interface: str ("sata", "sas", "ssd", "nvme", "scsi", or "iscsi") - } - } - interfaces: { - <intname>: { - "speed": int (mbit) - "nic_type": str ("onboard" or "pcie") - "order": int (compared to the other interfaces, indicates the "order" that the ports are laid out) - } - } - cpus: { - cores: int (hardware threads count) - architecture: str (x86_64" or "aarch64") - cpus: int (number of sockets) - cflags: str - } - ram: { - amount: int (GiB) - channels: int - } - } - """ - base_profile = ResourceProfile.objects.create(name=data['name'], description=data['description']) - base_profile.save() - - for lab_username in data['labs']: - lab = Lab.objects.get(lab_user__username=lab_username) - - base_profile.labs.add(lab) - base_profile.save() - - for diskname in data['disks'].keys(): - disk = data['disks'][diskname] - - disk_profile = DiskProfile.objects.create(name=diskname, size=disk['capacity'], media_type=disk['media_type'], interface=disk['interface'], host=base_profile) - disk_profile.save() - - for ifacename in data['interfaces'].keys(): - iface = data['interfaces'][ifacename] - - iface_profile = InterfaceProfile.objects.create(name=ifacename, speed=iface['speed'], nic_type=iface['nic_type'], order=iface['order'], host=base_profile) - iface_profile.save() - - cpu = data['cpus'] - cpu_prof = CpuProfile.objects.create(cores=cpu['cores'], architecture=cpu['architecture'], cpus=cpu['cpus'], cflags=cpu['cflags'], host=base_profile) - cpu_prof.save() - - ram_prof = RamProfile.objects.create(amount=data['ram']['amount'], channels=data['ram']['channels'], host=base_profile) - ram_prof.save() - - -def make_default_template(resource_profile, image_id=None, template_name=None, connected_interface_names=None, interfaces_tagged=False, connected_interface_tagged=False, owner_username="root", lab_username="unh_iol", public=True, temporary=False, description=""): - """ - Do not call this function without reading the related source code, it may have unintended effects. - - Used for creating a default template from some host profile - """ - - if not resource_profile: - raise Exception("No viable continuation from none resource_profile") - - if not template_name: - template_name = resource_profile.name - - if not connected_interface_names: - connected_interface_names = [InterfaceProfile.objects.filter(host=resource_profile).first().name] - print("setting connected interface names to", connected_interface_names) - - if not image_id: - image_id = Image.objects.filter(host_type=resource_profile).first().id - - image = Image.objects.get(id=image_id) - - base = ResourceTemplate.objects.create( - name=template_name, - xml="", - owner=User.objects.get(username=owner_username), - lab=Lab.objects.get(lab_user__username=lab_username), description=description, - public=public, temporary=temporary, copy_of=None) - - rconf = ResourceConfiguration.objects.create(profile=resource_profile, image=image, template=base, is_head_node=True, name="opnfv_host") - rconf.save() - - connected_interfaces = [] - - for iface_prof in InterfaceProfile.objects.filter(host=resource_profile).all(): - iface_conf = InterfaceConfiguration.objects.create(profile=iface_prof, resource_config=rconf) - - if iface_prof.name in connected_interface_names: - connected_interfaces.append(iface_conf) - - network = Network.objects.create(name="public", bundle=base, is_public=True) - - for iface in connected_interfaces: - connection = NetworkConnection.objects.create(network=network, vlan_is_tagged=interfaces_tagged) - connection.save() - - iface.connections.add(connection) - print("adding connection to iface ", iface) - iface.save() - connection.save() - - -def add_server(profile, name, interfaces, lab_username="unh_iol", vendor="unknown", model="unknown"): - """ - Used to enroll a new host of some profile - - @profile: the ResourceProfile in question (by reference to a model object) - - @name: the unique name of the server, currently indistinct from labid - - @interfaces: interfaces should be dict from interface name (eg ens1f0) to dict of schema: - { - mac_address: <mac addr>, - bus_addr: <bus addr>, //this field is optional, "" is default - } - - @lab_username: username of the lab to be added to - - @vendor: vendor name of the host, such as "HPE" or "Gigabyte" - - @model: specific model of the host, such as "DL380 Gen 9" - - """ - server = Server.objects.create( - bundle=None, - profile=profile, - config=None, - working=True, - vendor=vendor, - model=model, - labid=name, - lab=Lab.objects.get(lab_user__username=lab_username), - name=name, - booked=False) - - for iface_prof in InterfaceProfile.objects.filter(host=profile).all(): - mac_addr = interfaces[iface_prof.name]["mac_address"] - bus_addr = "unknown" - if "bus_addr" in interfaces[iface_prof.name].keys(): - bus_addr = interfaces[iface_prof.name]["bus_addr"] - - iface = Interface.objects.create(acts_as=None, profile=iface_prof, mac_address=mac_addr, bus_address=bus_addr) - iface.save() - - server.interfaces.add(iface) - server.save() - - -def extend_booking(booking_id, days=0, hours=0, minutes=0, weeks=0): - """ - Extend a booking by n <days, hours, minutes, weeks> - - @booking_id: id of the booking - - @days/@hours/@minutes/@weeks: the cumulative amount of delta to add to the length of the booking - """ - - booking = Booking.objects.get(id=booking_id) - booking.end = booking.end + timedelta(days=days, hours=hours, minutes=minutes, weeks=weeks) - booking.save() - - -def regenerate_cloud_configs(booking_id): - b = Booking.objects.get(id=booking_id) - for res in b.resource.get_resources(): - res.config.cloud_init_files.set(res.config.cloud_init_files.filter(generated=False)) # careful! - res.config.save() - cif = GeneratedCloudConfig.objects.create(resource_id=res.labid, booking=b, rconfig=res.config) - cif.save() - cif = CloudInitFile.create(priority=0, text=cif.serialize()) - cif.save() - res.config.cloud_init_files.add(cif) - res.config.save() - - -def set_job_new(job_id): - j = Job.objects.get(id=job_id) - b = j.booking - regenerate_cloud_configs(b.id) - for task in j.get_tasklist(): - task.status = JobStatus.NEW - task.save() - j.status = JobStatus.NEW - j.save() - - -def docs(function=None, fulltext=False): - """ - Print documentation for a given function in admin_utils. - Call without arguments for more information - """ - - fn = None - - if isinstance(function, str): - try: - fn = globals()[function] - except KeyError: - print("Couldn't find a function by the given name") - return - elif callable(function): - fn = function - else: - print("docs(function: callable | str, fulltext: bool) was called with a 'function' that was neither callable nor a string name of a function") - print("usage: docs('some_function_in_admin_utils', fulltext=True)") - print("The 'fulltext' argument is used to choose if you want the complete source of the function printed. If this argument is false then you will only see the pydoc rendered documentation for the function") - return - - if not fn: - print("couldn't find a function by that name") - - if not fulltext: - print("Pydoc documents the function as such:") - print(pydoc.render_doc(fn)) - else: - print("The full source of the function is this:") - print(inspect.getsource(fn)) - - -def admin_functions(): - """ - List functions available to call within admin_utils - """ - - return [name for name, func in inspect.getmembers(sys.modules[__name__]) if (inspect.isfunction(func) and func.__module__ == __name__)] - - -print("Hint: call `docs(<function name>)` or `admin_functions()` for help on using the admin utils") -print("docs(<function name>) displays documentation on a given function") -print("admin_functions() lists all functions available to call within this module") diff --git a/src/dashboard/populate_db_iol.py b/src/dashboard/populate_db_iol.py deleted file mode 100644 index d8df03f..0000000 --- a/src/dashboard/populate_db_iol.py +++ /dev/null @@ -1,352 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 json -import yaml - -from account.models import Lab, UserProfile -from django.contrib.auth.models import User -from resource_inventory.models import ( - HostProfile, - InterfaceProfile, - DiskProfile, - CpuProfile, - RamProfile, - VlanManager, - Scenario, - Installer, - Opsys, - OPNFVRole, - Image, - Interface, - Host -) - - -class Populator: - - def __init__(self): - self.host_profile_count = 0 - self.generic_host_count = 0 - self.host_profiles = [] - self.generic_bundle_count = 0 - self.booking_count = 0 - - def make_host_profile(self, lab, data): - hostProfile = HostProfile.objects.create( - host_type=data['host']['type'], - name=data['host']['name'], - description=data['host']['description'] - ) - hostProfile.save() - - for iface_data in data['interfaces']: - - interfaceProfile = InterfaceProfile.objects.create( - speed=iface_data['speed'], - name=iface_data['name'], - host=hostProfile - ) - interfaceProfile.save() - - for disk_data in data['disks']: - - diskProfile = DiskProfile.objects.create( - size=disk_data['size'], - media_type=disk_data['type'], - name=disk_data['name'], - host=hostProfile - ) - diskProfile.save() - - cpuProfile = CpuProfile.objects.create( - cores=data['cpu']['cores'], - architecture=data['cpu']['arch'], - cpus=data['cpu']['cpus'], - host=hostProfile - ) - cpuProfile.save() - ramProfile = RamProfile.objects.create( - amount=data['ram']['amount'], - channels=data['ram']['channels'], - host=hostProfile - ) - ramProfile.save() - hostProfile.labs.add(lab) - return hostProfile - - def make_users(self): - user_pberberian = User.objects.create(username="pberberian") - user_pberberian.save() - user_pberberian_prof = UserProfile.objects.create(user=user_pberberian) - user_pberberian_prof.save() - - user_sbergeron = User.objects.create(username="sbergeron") - user_sbergeron.save() - user_sbergeron_prof = UserProfile.objects.create(user=user_sbergeron) - user_sbergeron_prof.save() - return [user_sbergeron, user_pberberian] - - def make_labs(self): - unh_iol = User.objects.create(username="unh_iol") - unh_iol.save() - vlans = [] - reserved = [] - for i in range(1, 4096): - vlans.append(1) - reserved.append(0) - iol = Lab.objects.create( - lab_user=unh_iol, - name="UNH_IOL", - vlan_manager=VlanManager.objects.create( - vlans=json.dumps(vlans), - reserved_vlans=json.dumps(reserved), - allow_overlapping=False, - block_size=20, - ), - api_token=Lab.make_api_token(), - contact_email="nfv-lab@iol.unh.edu", - location="University of New Hampshire, Durham NH, 03824 USA" - ) - return [iol] - - def make_configurations(self): - # scenarios - scen1 = Scenario.objects.create(name="os-nosdn-nofeature-noha") - scen2 = Scenario.objects.create(name="os-odl-kvm-ha") - scen3 = Scenario.objects.create(name="os-nosdn-nofeature-ha") - - # installers - fuel = Installer.objects.create(name="Fuel") - fuel.sup_scenarios.add(scen1) - fuel.sup_scenarios.add(scen3) - fuel.save() - joid = Installer.objects.create(name="Joid") - joid.sup_scenarios.add(scen1) - joid.sup_scenarios.add(scen2) - joid.save() - apex = Installer.objects.create(name="Apex") - apex.sup_scenarios.add(scen2) - apex.sup_scenarios.add(scen3) - apex.save() - daisy = Installer.objects.create(name="Daisy") - daisy.sup_scenarios.add(scen1) - daisy.sup_scenarios.add(scen2) - daisy.sup_scenarios.add(scen3) - daisy.save() - compass = Installer.objects.create(name="Compass") - compass.sup_scenarios.add(scen1) - compass.sup_scenarios.add(scen3) - compass.save() - - # operating systems - ubuntu = Opsys.objects.create(name="Ubuntu") - ubuntu.sup_installers.add(compass) - ubuntu.sup_installers.add(joid) - ubuntu.save() - centos = Opsys.objects.create(name="CentOs") - centos.sup_installers.add(apex) - centos.sup_installers.add(fuel) - centos.save() - suse = Opsys.objects.create(name="Suse") - suse.sup_installers.add(fuel) - suse.save() - - # opnfv roles - OPNFVRole.objects.create(name="Compute", description="Does the heavy lifting") - OPNFVRole.objects.create(name="Controller", description="Controls everything") - OPNFVRole.objects.create(name="Jumphost", description="Entry Point") - - lab = Lab.objects.first() - user = UserProfile.objects.first().user - Image.objects.create( - lab_id=23, - name="hpe centos", - from_lab=lab, - owner=user, - host_type=HostProfile.objects.get(name="hpe") - ) - Image.objects.create( - lab_id=25, - name="hpe ubuntu", - from_lab=lab, - owner=user, - host_type=HostProfile.objects.get(name="hpe") - ) - - Image.objects.create( - lab_id=26, - name="hpe suse", - from_lab=lab, - owner=user, - host_type=HostProfile.objects.get(name="hpe") - ) - - Image.objects.create( - lab_id=27, - name="arm ubuntu", - from_lab=lab, - owner=user, - host_type=HostProfile.objects.get(name="arm") - ) - - def make_lab_hosts(self, hostcount, profile, lab, data, offset=1): - for i in range(hostcount): - name = "Host_" + lab.name + "_" + profile.name + "_" + str(i + offset) - host = Host.objects.create( - name=name, - lab=lab, - profile=profile, - labid=data[i]['labid'] - ) - for iface_profile in profile.interfaceprofile.all(): - iface_data = data[i]['interfaces'][iface_profile.name] - Interface.objects.create( - mac_address=iface_data['mac'], - bus_address=iface_data['bus'], - name=iface_profile.name, - host=host - ) - - def make_profile_data(self): - """ - Create Profile Data. - - returns a dictionary of data from the yaml files - created by inspection scripts - """ - data = [] - for prof in ["hpe", "arm"]: # TODO - profile_dict = {} - host = { - "name": prof, - "type": 0, - "description": "some LaaS servers" - } - profile_dict['host'] = host - profile_dict['interfaces'] = [] - for interface in [{"name": "eno1", "speed": 1000}, {"name": "eno2", "speed": 10000}]: # TODO - iface_dict = {} - iface_dict["name"] = interface['name'] - iface_dict['speed'] = interface['speed'] - profile_dict['interfaces'].append(iface_dict) - - profile_dict['disks'] = [] - for disk in [{"size": 1000, "type": "ssd", "name": "sda"}]: # TODO - disk_dict = {} - disk_dict['size'] = disk['size'] - disk_dict['type'] = disk['type'] - disk_dict['name'] = disk['name'] - profile_dict['disks'].append(disk_dict) - - # cpu - cpu = {} - cpu['cores'] = 4 - cpu['arch'] = "x86" - cpu['cpus'] = 2 - profile_dict['cpu'] = cpu - - # ram - ram = {} - ram['amount'] = 256 - ram['channels'] = 4 - profile_dict['ram'] = ram - - data.append(profile_dict) - - return data - - def get_lab_data(self, lab): - data = {} - path = "/laas_dashboard/data/" + lab.name + "/" - host_file = open(path + "hostlist.json") - host_structure = json.loads(host_file.read()) - host_file.close() - for profile in host_structure['profiles'].keys(): - data[profile] = {} - prof_path = path + profile - for host in host_structure['profiles'][profile]: - host_file = open(prof_path + "/" + host + ".yaml") - host_data = yaml.load(host_file.read()) - host_file.close() - data[profile][host] = host_data - return data - - def make_profiles_and_hosts(self, lab, lab_data): - for host_profile_name, host_data_dict in lab_data.items(): - if len(host_data_dict) < 1: - continue - host_profile = HostProfile.objects.create( - name=host_profile_name, - description="" - ) - host_profile.labs.add(lab) - example_host_data = list(host_data_dict.values())[0] - - cpu_data = example_host_data['cpu'] - CpuProfile.objects.create( - cores=cpu_data['cores'], - architecture=cpu_data['arch'], - cpus=cpu_data['cpus'], - host=host_profile - ) - - ram_data = example_host_data['memory'] - RamProfile.objects.create( - amount=int(ram_data[:-1]), - channels=1, - host=host_profile - ) - - disks_data = example_host_data['disk'] - for disk_data in disks_data: - size = 0 - try: - size = int(disk_data['size'].split('.')[0]) - except Exception: - size = int(disk_data['size'].split('.')[0][:-1]) - DiskProfile.objects.create( - size=size, - media_type="SSD", - name=disk_data['name'], - host=host_profile - ) - - ifaces_data = example_host_data['interface'] - for iface_data in ifaces_data: - InterfaceProfile.objects.create( - speed=iface_data['speed'], - name=iface_data['name'], - host=host_profile - ) - - # all profiles created - for hostname, host_data in host_data_dict.items(): - host = Host.objects.create( - name=hostname, - labid=hostname, - profile=host_profile, - lab=lab - ) - for iface_data in host_data['interface']: - Interface.objects.create( - mac_address=iface_data['mac'], - bus_address=iface_data['busaddr'], - name=iface_data['name'], - host=host - ) - - def populate(self): - self.labs = self.make_labs() - # We should use the existing users, not creating our own - for lab in self.labs: - lab_data = self.get_lab_data(lab) - self.make_profiles_and_hosts(lab, lab_data) - - # We will add opnfv info and images as they are created and supported diff --git a/src/dashboard/tasks.py b/src/dashboard/tasks.py index 93e6a22..ac4b006 100644 --- a/src/dashboard/tasks.py +++ b/src/dashboard/tasks.py @@ -9,90 +9,23 @@ ############################################################################## +from booking.models import Booking 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, - JobFactory -) - -from resource_inventory.resource_manager import ResourceManager -from resource_inventory.models import ConfigState - +from api.views import liblaas_end_booking +# todo - make a task to check for expired bookings @shared_task -def booking_poll(): - def cleanup_resource_task(qs): - for hostrelation in qs: - hostrelation.config.state = ConfigState.CLEAN - hostrelation.config.save() - hostrelation.status = JobStatus.NEW - hostrelation.save() - - def cleanup_software(qs): - if qs.exists(): - relation = qs.first() - software = relation.config.opnfv - software.clear_delta() - software.save() - relation.status = JobStatus.NEW - relation.save() - - def cleanup_access(qs): - for relation in qs: - if "vpn" in relation.config.access_type.lower(): - relation.config.set_revoke(True) - relation.config.save() - relation.status = JobStatus.NEW - relation.save() - - cleanup_set = Booking.objects.filter(end__lte=timezone.now()).filter(job__complete=False) - +def end_expired_bookings(): + print("Celery task for end_expired_bookings() has been triggered") + cleanup_set = Booking.objects.filter(end__lte=timezone.now(), ).filter(complete=False) + print("Newly expired bookings: ", cleanup_set) for booking in cleanup_set: - if not booking.job.complete: - job = booking.job - cleanup_software(SoftwareRelation.objects.filter(job=job)) - cleanup_resource_task(HostHardwareRelation.objects.filter(job=job)) - cleanup_resource_task(HostNetworkRelation.objects.filter(job=job)) - cleanup_access(AccessRelation.objects.filter(job=job)) - job.complete = True - job.save() - NotificationHandler.notify_booking_end(booking) - - -@shared_task -def free_hosts(): - """Free all hosts that should be freed.""" - undone_statuses = [JobStatus.NEW, JobStatus.CURRENT, JobStatus.ERROR] - undone_jobs = Job.objects.filter( - hostnetworkrelation__status__in=undone_statuses, - hosthardwarerelation__status__in=undone_statuses - ) - - bookings = Booking.objects.exclude( - job__in=undone_jobs - ).filter( - end__lt=timezone.now(), - job__complete=True, - complete=False, - resource__isnull=False, - ) - - for booking in bookings: - ResourceManager.getInstance().releaseResourceBundle(booking.resource) booking.complete = True - print("Booking", booking.id, "is now completed") + if (booking.aggregateId): + print("ending booking " + str(booking.id) + " with agg id: ", booking.aggregateId) + liblaas_end_booking(booking.aggregateId) + else: + print("booking " + str(booking.id) + " has no agg id") booking.save() - - -@shared_task -def query_vpn_users(): - """ get active vpn users """ - JobFactory.makeActiveUsersTask() + print("Finished end_expired_bookings()") diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py deleted file mode 100644 index 5be6379..0000000 --- a/src/dashboard/testing_utils.py +++ /dev/null @@ -1,315 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.auth.models import User -from django.core.files.base import ContentFile -from django.utils import timezone - -import json -from datetime import timedelta - -from booking.models import Booking -from account.models import UserProfile, Lab, LabStatus, VlanManager, PublicNetwork -from resource_inventory.models import ( - ResourceTemplate, - ResourceProfile, - ResourceConfiguration, - InterfaceProfile, - InterfaceConfiguration, - Server, - DiskProfile, - CpuProfile, - Opsys, - Image, - Scenario, - Installer, - OPNFVRole, - RamProfile, - Network, -) -from resource_inventory.resource_manager import ResourceManager - -""" -Info for make_booking() function: -[topology] argument structure: - the [topology] argument should describe the structure of the pod - the top level should be a dictionary, with each key being a hostname - each value in the top level should be a dictionary with two keys: - "type" should map to a host profile instance - "nets" should map to a list of interfaces each with a list of - dictionaries each defining a network in the format - { "name": "netname", "tagged": True|False, "public": True|False } - each network is defined if a matching name is not found - - sample argument structure: - topology={ - "host1": { - "type": instanceOf HostProfile, - "role": instanceOf OPNFVRole - "image": instanceOf Image - "nets": [ - 0: [ - 0: { "name": "public", "tagged": True, "public": True }, - 1: { "name": "private", "tagged": False, "public": False }, - ] - 1: [] - ] - } - } -""" - - -def make_booking(owner=None, start=timezone.now(), - end=timezone.now() + timedelta(days=1), - lab=None, purpose="my_purpose", - project="my_project", collaborators=[], - topology={}, installer=None, scenario=None): - - resource_template = make_resource_template() - resource = ResourceManager.getInstance().convertResourceBundle(resource_template) - if not resource: - raise Exception("Resource not created") - - return Booking.objects.create( - resource=resource, - start=start, - end=end, - owner=owner, - purpose=purpose, - project=project, - lab=lab, - ) - - -def make_network(name, lab, grb, public): - network = Network(name=name, bundle=grb, is_public=public) - if public: - public_net = lab.vlan_manager.get_public_vlan() - if not public_net: - raise Exception("No more public networks available") - lab.vlan_manager.reserve_public_vlan(public_net.vlan) - network.vlan_id = public_net.vlan - else: - 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_nets) - network.vlan_id = private_nets[0] - - network.save() - return network - - -def make_resource_template(owner=None, lab=None, name="Test Template"): - if owner is None: - owner = make_user(username="template_owner") - if lab is None: - lab = make_lab(name="template_lab") - rt = ResourceTemplate.objects.create(name=name, owner=owner, lab=lab, public=True) - config = make_resource_config(rt) - make_interface_config(config) - return rt - - -def make_resource_config(template, profile=None, image=None): - if profile is None: - profile = make_resource_profile(lab=template.lab) - - if image is None: - image = make_image(profile, lab=template.lab) - - return ResourceConfiguration.objects.create(profile=profile, image=image, template=template) - - -def make_interface_config(resource_config): - # lets just grab one of the iface profiles from the related host - iface_profile = resource_config.profile.interfaceprofile.all()[0] - - # not adding any connections here - return InterfaceConfiguration.objects.create(profile=iface_profile, resource_config=resource_config) - - -def make_user(is_superuser=False, username="testuser", - password="testpassword", email="default_email@user.com"): - user = User.objects.get_or_create(username=username, email=email, password=password)[0] - - user.is_superuser = is_superuser - user.save() - - return user - - -def make_user_profile(user=None, email_addr="email@email.com", - company="company", full_name="John Doe", - booking_privledge=True, ssh_file=None): - user = user or make_user() - profile = UserProfile.objects.get_or_create( - email_addr=email_addr, - company=company, - full_name=full_name, - booking_privledge=booking_privledge, - user=user - )[0] - profile.ssh_public_key.save("user_ssh_key", ssh_file if ssh_file else ContentFile("public key content string")) - - return profile - - -def make_vlan_manager(vlans=None, block_size=20, allow_overlapping=False, reserved_vlans=None): - if not vlans: - vlans = [vlan % 2 for vlan in range(4095)] - if not reserved_vlans: - reserved_vlans = [0 for i in range(4095)] - - return VlanManager.objects.create( - vlans=json.dumps(vlans), - reserved_vlans=json.dumps(vlans), - block_size=block_size, - allow_overlapping=allow_overlapping - ) - - -def make_lab(user=None, name="Test_Lab_Instance", - status=LabStatus.UP, vlan_manager=None, - pub_net_count=5): - if Lab.objects.filter(name=name).exists(): - return Lab.objects.get(name=name) - - if not vlan_manager: - vlan_manager = make_vlan_manager() - - if not user: - user = make_user(username=name + " user") - - lab = Lab.objects.create( - lab_user=user, - name=name, - contact_email='test_lab@test_site.org', - contact_phone='603 123 4567', - status=status, - vlan_manager=vlan_manager, - description='test lab instantiation', - api_token='12345678' - ) - - for i in range(pub_net_count): - make_public_net(vlan=i * 2 + 1, lab=lab) - - return lab - - -""" -resource_inventory instantiation section for permanent resources -""" - - -def make_resource_profile(lab, name="test_hostprofile"): - if ResourceProfile.objects.filter(name=name).exists(): - return ResourceProfile.objects.get(name=name) - - resource_profile = ResourceProfile.objects.create( - name=name, - description='test resourceprofile instance' - ) - resource_profile.labs.add(lab) - - RamProfile.objects.create(host=resource_profile, amount=8, channels=2) - CpuProfile.objects.create(cores=4, architecture="x86_64", cpus=1, host=resource_profile) - DiskProfile.objects.create( - name="test disk profile", - size=256, - media_type="SSD", - host=resource_profile - ) - - InterfaceProfile.objects.create( - host=resource_profile, - name="test interface profile", - speed=1000, - nic_type="pcie" - ) - - return resource_profile - - -def make_image(resource_profile, lab=None, lab_id="4", owner=None, os=None, - public=True, name="default image", description="default image"): - if lab is None: - lab = make_lab() - - if owner is None: - owner = make_user() - - if os is None: - os = make_os() - - return Image.objects.create( - from_lab=lab, - lab_id=lab_id, - os=os, - host_type=resource_profile, - public=public, - name=name, - description=description - ) - - -def make_scenario(name="test scenario"): - return Scenario.objects.create(name=name) - - -def make_installer(scenarios, name="test installer"): - installer = Installer.objects.create(name=name) - for scenario in scenarios: - installer.sup_scenarios.add(scenario) - - return installer - - -def make_os(installers=None, name="test OS"): - if not installers: - installers = [make_installer([make_scenario()])] - os = Opsys.objects.create(name=name) - for installer in installers: - os.sup_installers.add(installer) - - return os - - -def make_server(host_profile, lab, labid="test_host", name="test_host", - booked=False, working=True, config=None, - bundle=None, model="Model 1", vendor="ACME"): - return Server.objects.create( - lab=lab, - profile=host_profile, - name=name, - booked=booked, - working=working, - config=config, - bundle=bundle, - model=model, - vendor=vendor - ) - - -def make_opnfv_role(name="Jumphost", description="test opnfvrole"): - return OPNFVRole.objects.create( - name=name, - description=description - ) - - -def make_public_net(vlan, lab, in_use=False, - cidr="0.0.0.0/0", gateway="0.0.0.0"): - return PublicNetwork.objects.create( - lab=lab, - vlan=vlan, - cidr=cidr, - gateway=gateway - ) diff --git a/src/dashboard/tests/__init__.py b/src/dashboard/tests/__init__.py deleted file mode 100644 index b6fef6c..0000000 --- a/src/dashboard/tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# 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/src/dashboard/tests/test_views.py b/src/dashboard/tests/test_views.py deleted file mode 100644 index f2d5490..0000000 --- a/src/dashboard/tests/test_views.py +++ /dev/null @@ -1,30 +0,0 @@ -############################################################################## -# Copyright (c) 2020 Parker Berberian, Sawyer Bergeron, 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, Client -from dashboard.testing_utils import make_lab - - -class DashboardViewTestCase(TestCase): - @classmethod - def setUpTestData(cls): - make_lab(name="TestLab") - cls.client = Client() - - def test_landing_view_anon(self): - response = self.client.get('/') - self.assertEqual(response.status_code, 200) - - def test_lab_list_view(self): - response = self.client.get('/lab/') - self.assertEqual(response.status_code, 200) - - def test_lab_detail_view(self): - response = self.client.get('/lab/TestLab/') - self.assertEqual(response.status_code, 200) diff --git a/src/dashboard/views.py b/src/dashboard/views.py index ff26c64..2942d59 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -19,8 +19,6 @@ 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 from laas_dashboard import settings @@ -33,17 +31,20 @@ def lab_list_view(request): def lab_detail_view(request, lab_name): + # todo - LL Integration user = None if request.user.is_authenticated: user = request.user lab = get_object_or_404(Lab, name=lab_name) - images = Image.objects.filter(from_lab=lab).filter(public=True) - if user: - images = images | Image.objects.filter(from_lab=lab).filter(owner=user) + # images = Image.objects.filter(from_lab=lab).filter(public=True) + images = [] + # if user: + # images = images | Image.objects.filter(from_lab=lab).filter(owner=user) - hosts = ResourceQuery.filter(lab=lab) + # hosts = ResourceQuery.filter(lab=lab) + hosts = [] return render( request, @@ -51,7 +52,7 @@ def lab_detail_view(request, lab_name): { 'title': "Lab Overview", 'lab': lab, - 'hostprofiles': ResourceProfile.objects.filter(labs=lab), + # 'hostprofiles': ResourceProfile.objects.filter(labs=lab), 'images': images, 'hosts': hosts } @@ -70,7 +71,6 @@ 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( @@ -85,7 +85,6 @@ def landing_view(request): request, 'dashboard/landing.html', { - 'manager': manager is not None, 'title': "Welcome to the Lab as a Service Dashboard", 'bookings': bookings, 'LFID': LFID @@ -99,21 +98,4 @@ class LandingView(TemplateView): def get_context_data(self, **kwargs): context = super(LandingView, self).get_context_data(**kwargs) - hosts = [] - - for host_profile in ResourceProfile.objects.all(): - name = host_profile.name - description = host_profile.description - in_labs = host_profile.labs - - interfaces = host_profile.interfaceprofile - storage = host_profile.storageprofile - cpu = host_profile.cpuprofile - ram = host_profile.ramprofile - - host = (name, description, in_labs, interfaces, storage, cpu, ram) - hosts.append(host) - - context.update({'hosts': hosts}) - return context diff --git a/src/laas_dashboard/model_test.py b/src/laas_dashboard/model_test.py deleted file mode 100644 index ba3ef35..0000000 --- a/src/laas_dashboard/model_test.py +++ /dev/null @@ -1,110 +0,0 @@ -############################################################################## -# Copyright (c) 2020 Sawyer Bergeron, Parker Berberian, 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 resource_inventory.models import ( - ResourceProfile, - ResourceQuery, - Image, - DiskProfile, - CpuProfile, - RamProfile, - InterfaceProfile, -) - - -def rp_has_all_components(): - """ - Check that every ResourceProfile has an InterfaceProfile, - DiskProfile, CpuProfile, and RamProfile. - """ - - result = True - - for rp in ResourceProfile.objects.all(): - ip = InterfaceProfile.objects.filter(host=rp).exists() - dp = DiskProfile.objects.filter(host=rp).exists() - cp = CpuProfile.objects.filter(host=rp).exists() - ram = RamProfile.objects.filter(host=rp).exists() - - if not ip: - print("No InterfaceProfiles for host", rp.name) - result = False - - if not dp: - print("No DiskProfile for host", rp.name) - result = False - - if not cp: - print("No CpuProfile for host", rp.name) - result = False - - if not ram: - print("No RamProfile for host", rp.name) - result = False - - return result - - -def ip_for_all_ifaces(): - """ - Check that every InterfaceProfile for a Resource has - an Interface. - """ - - result = True - - for res in ResourceQuery.filter(): - iface_set = res.get_interfaces() - iface_profile_set = InterfaceProfile.objects.filter(host=res.profile) - - # find out what profiles we have - curr_profiles = [iface.profile for iface in iface_set] - missing_profiles = set(iface_profile_set) - set(curr_profiles) - - if missing_profiles: - print('No interface for profiles', missing_profiles, 'for host', res.name) - result = False - - return result - - -def rp_has_image(): - """ - Make sure every ResourceProfile has an Image. - """ - - result = True - - rp_set = ResourceProfile.objects.all() - image_set = Image.objects.all() - image_profiles = set([image.host_type for image in image_set]) - - for rp in rp_set: - if rp not in image_profiles: - print("ResourceProfile", rp.name, "has no image associated with it.") - result = False - return result - - -def run_test(test): - print('RUNNING TEST', test) - result = test() - if result: - print(test, 'WAS A SUCCESS!') - else: - print(test, 'FAILED') - print('============================================') - - -def run_tests(): - tests = [rp_has_all_components, ip_for_all_ifaces, rp_has_image] - - for test in tests: - run_test(test) diff --git a/src/laas_dashboard/settings.py b/src/laas_dashboard/settings.py index 7e27c8d..66de35d 100644 --- a/src/laas_dashboard/settings.py +++ b/src/laas_dashboard/settings.py @@ -220,35 +220,13 @@ RABBITMQ_DEFAULT_PASS = os.environ['RABBITMQ_DEFAULT_PASS'] CELERY_BROKER_URL = 'amqp://' + RABBITMQ_DEFAULT_USER + ':' + RABBITMQ_DEFAULT_PASS + '@rabbitmq:5672//' CELERY_BEAT_SCHEDULE = { + # Keeping commented as an example for the future 'booking_poll': { - 'task': 'dashboard.tasks.booking_poll', + 'task': 'dashboard.tasks.end_expired_bookings', 'schedule': timedelta(minutes=1) - }, - 'free_hosts': { - 'task': 'dashboard.tasks.free_hosts', - 'schedule': timedelta(minutes=1) - }, - 'notify_expiring': { - 'task': 'notifier.tasks.notify_expiring', - 'schedule': timedelta(hours=1) - }, - 'query_vpn_users': { - 'task': 'dashboard.tasks.query_vpn_users', - 'schedule': timedelta(hours=1) - }, - 'dispatch_emails': { - 'task': 'notifier.tasks.dispatch_emails', - 'schedule': timedelta(minutes=10) } } # Notifier Settings -EMAIL_HOST = os.environ.get('EMAIL_HOST') -EMAIL_PORT = os.environ.get('EMAIL_PORT') -EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') -EMAIL_USE_TLS = True -DEFAULT_EMAIL_FROM = os.environ.get('DEFAULT_EMAIL_FROM', 'webmaster@localhost') -SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" EXPIRE_LIFETIME = 12 # Minimum lifetime of booking to send notification EXPIRE_HOURS = 48 # Notify when booking is expiring within this many hours diff --git a/src/laas_dashboard/urls.py b/src/laas_dashboard/urls.py index 7a37d7e..f78e9ff 100644 --- a/src/laas_dashboard/urls.py +++ b/src/laas_dashboard/urls.py @@ -37,11 +37,9 @@ urlpatterns = [ url(r'^', include('dashboard.urls', namespace='dashboard')), url(r'^booking/', include('booking.urls', namespace='booking')), url(r'^accounts/', include('account.urls', namespace='account')), - url(r'^resource/', include('resource_inventory.urls', namespace='resource')), url(r'^admin/', admin.site.urls), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api/', include('api.urls')), - url(r'^messages/', include('notifier.urls', namespace='notifier')), url(r'^oidc/', include('mozilla_django_oidc.urls')), ] diff --git a/src/notifier/admin.py b/src/notifier/admin.py index f6dbfd1..c7e20d6 100644 --- a/src/notifier/admin.py +++ b/src/notifier/admin.py @@ -8,8 +8,3 @@ ############################################################################## from django.contrib import admin - -from notifier.models import Notification, Emailed - -admin.site.register(Notification) -admin.site.register(Emailed) diff --git a/src/notifier/manager.py b/src/notifier/manager.py deleted file mode 100644 index e2afdec..0000000 --- a/src/notifier/manager.py +++ /dev/null @@ -1,162 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron and others. -# Copyright (c) 2020 Sawyer Bergeron, 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 -############################################################################## -import os -from notifier.models import Notification, Emailed, Email - -from django.template.loader import render_to_string -from django.utils import timezone - - -class NotificationHandler(object): - - @classmethod - def notify_new_booking(cls, booking): - template = "notifier/new_booking.html" - titles = ["You have a new booking (" + str(booking.id) + ")", "You have been added to a booking (" + str(booking.id) + ")"] - cls.booking_notify(booking, template, titles) - - @classmethod - def notify_booking_end(cls, booking): - template = "notifier/end_booking.html" - titles = ["Your booking (" + str(booking.id) + ") has ended", "A booking (" + str(booking.id) + ") that you collaborate on has ended"] - cls.booking_notify(booking, template, titles) - - @classmethod - def notify_booking_expiring(cls, booking): - template = "notifier/expiring_booking.html" - titles = ["Your booking (" + str(booking.id) + ") is about to expire", "A booking (" + str(booking.id) + ") that you collaborate on is about to expire"] - cls.booking_notify(booking, template, titles) - cls.email_booking_expiring(booking) - - @classmethod - def booking_notify(cls, booking, template, titles): - """ - Create a notification for a booking owner and collaborators using the template. - - titles is a list - the first is the title for the owner's notification, - the last is the title for the collaborators' - """ - owner_notif = Notification.objects.create( - title=titles[0], - content=render_to_string( - template, - context={ - "booking": booking, - "owner": True - } - ) - ) - owner_notif.recipients.add(booking.owner.userprofile) - if not booking.collaborators.all().exists(): - return # no collaborators - were done - - collab_notif = Notification.objects.create( - title=titles[-1], - content=render_to_string( - template, - context={ - "booking": booking, - "owner": False - } - ) - ) - for c in booking.collaborators.all(): - collab_notif.recipients.add(c.userprofile) - - @classmethod - def email_job_fulfilled(cls, job): - template_name = "notifier/email_fulfilled.txt" - all_tasks = job.get_tasklist() - users = list(job.booking.collaborators.all()) - users.append(job.booking.owner) - for user in users: - user_tasklist = [] - # gather up all the relevant messages from the lab - for task in all_tasks: - if (not hasattr(task.config, "user")) or task.config.user == user: - user_tasklist.append( - { - "title": task.type_str() + " Message: ", - "content": task.message - } - ) - # gather up all the other needed info - context = { - "owner": user == job.booking.owner, - "user_name": user.userprofile.full_name, - "messages": user_tasklist, - "booking_url": os.environ.get("DASHBOARD_URL", "<Dashboard url>") + "/booking/detail/" + str(job.booking.id) + "/" - } - - # render email template - message = render_to_string(template_name, context) - - # finally, queue email for sending - Email.objects.create(title="Your Booking is Ready", message=message, recipient=user.userprofile.email_addr) - - @classmethod - def email_booking_over(cls, booking): - template_name = "notifier/email_ended.txt" - hostnames = [host.name for host in booking.resource.get_resources()] - users = list(booking.collaborators.all()) - users.append(booking.owner) - for user in users: - context = { - "user_name": user.userprofile.full_name, - "booking": booking, - "hosts": hostnames, - "booking_url": os.environ.get("DASHBOARD_URL", "<Dashboard url>") + "/booking/detail/" + str(booking.id) + "/" - } - - message = render_to_string(template_name, context) - - Email.objects.create(title="Your Booking has Expired", message=message, recipient=user.userprofile.email_addr) - - @classmethod - def email_booking_expiring(cls, booking): - template_name = "notifier/email_expiring.txt" - hostnames = [host.name for host in booking.resource.get_resources()] - users = list(booking.collaborators.all()) - users.append(booking.owner) - for user in users: - context = { - "user_name": user.userprofile.full_name, - "booking": booking, - "hosts": hostnames, - "booking_url": os.environ.get("DASHBOARD_URL", "<Dashboard url>") + "/booking/detail/" + str(booking.id) + "/" - } - - message = render_to_string(template_name, context) - - Email.objects.create(title="Your Booking is Expiring", message=message, recipient=user.userprofile.email_addr) - - @classmethod - def task_updated(cls, task): - """ - Notification of task changing. - - called every time a lab updated info about a task. - sends an email when 'task' changing state means a booking has - just been fulfilled (all tasks done, servers ready to use) - or is over. - """ - if task.job is None or task.job.booking is None: - return - if task.job.is_fulfilled(): - if task.job.booking.end < timezone.now(): - if Emailed.objects.filter(end_booking=task.job.booking).exists(): - return - Emailed.objects.create(end_booking=task.job.booking) - cls.email_booking_over(task.job.booking) - if task.job.booking.end > timezone.now() and task.job.booking.start < timezone.now(): - if Emailed.objects.filter(begin_booking=task.job.booking).exists(): - return - Emailed.objects.create(begin_booking=task.job.booking) - cls.email_job_fulfilled(task.job) diff --git a/src/notifier/migrations/0008_auto_20230608_1913.py b/src/notifier/migrations/0008_auto_20230608_1913.py new file mode 100644 index 0000000..898c300 --- /dev/null +++ b/src/notifier/migrations/0008_auto_20230608_1913.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifier', '0007_email'), + ] + + operations = [ + migrations.DeleteModel( + name='Email', + ), + migrations.RemoveField( + model_name='notification', + name='read_by', + ), + migrations.RemoveField( + model_name='notification', + name='recipients', + ), + migrations.DeleteModel( + name='Emailed', + ), + migrations.DeleteModel( + name='Notification', + ), + ] diff --git a/src/notifier/models.py b/src/notifier/models.py index 03e23b3..f903394 100644 --- a/src/notifier/models.py +++ b/src/notifier/models.py @@ -6,51 +6,3 @@ # 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 UserProfile -from booking.models import Booking - - -class Notification(models.Model): - title = models.CharField(max_length=150) - content = models.TextField() - recipients = models.ManyToManyField(UserProfile, related_name='notifications') - is_html = models.BooleanField(default=True) - read_by = models.ManyToManyField(UserProfile, related_name='read_notifications') - - def __str__(self): - return self.title - - def to_preview_html(self): - return "<h3>" + self.title + "</h3>" # TODO - template? - - -class Emailed(models.Model): - """A simple record to remember who has already gotten an email to avoid resending.""" - - begin_booking = models.OneToOneField( - Booking, - null=True, - on_delete=models.CASCADE, - related_name="begin_mail" - ) - almost_end_booking = models.OneToOneField( - Booking, - null=True, - on_delete=models.CASCADE, - related_name="warning_mail" - ) - end_booking = models.OneToOneField( - Booking, - null=True, - on_delete=models.CASCADE, - related_name="over_mail" - ) - - -class Email(models.Model): - sent = models.BooleanField(default=False) - title = models.CharField(max_length=150) - message = models.TextField() - recipient = models.CharField(max_length=150) diff --git a/src/notifier/tasks.py b/src/notifier/tasks.py deleted file mode 100644 index 64d7574..0000000 --- a/src/notifier/tasks.py +++ /dev/null @@ -1,51 +0,0 @@ -############################################################################## -# Copyright (c) 2016 Max Breitenfeldt and others. -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 celery import shared_task -from django.utils import timezone -from django.conf import settings -from booking.models import Booking -from notifier.models import Emailed, Email -from notifier.manager import NotificationHandler -from django.core.mail import send_mail - -import os - - -@shared_task -def notify_expiring(): - """Notify users if their booking is within 48 hours of expiring.""" - expire_time = timezone.now() + timezone.timedelta(hours=settings.EXPIRE_HOURS) - # Don't email people about bookings that have started recently - start_time = timezone.now() - timezone.timedelta(hours=settings.EXPIRE_LIFETIME) - bookings = Booking.objects.filter( - end__lte=expire_time, - end__gte=timezone.now(), - start__lte=start_time - ) - for booking in bookings: - if Emailed.objects.filter(almost_end_booking=booking).exists(): - continue - NotificationHandler.notify_booking_expiring(booking) - Emailed.objects.create(almost_end_booking=booking) - - -@shared_task -def dispatch_emails(): - for email in Email.objects.filter(sent=False): - email.sent = True - email.save() - send_mail( - email.title, - email.message, - os.environ.get("DEFAULT_FROM_EMAIL", "opnfv@laas-dashboard"), - [email.recipient], - fail_silently=False) diff --git a/src/notifier/tests/test_dispatcher.py b/src/notifier/tests/test_dispatcher.py deleted file mode 100644 index 086f621..0000000 --- a/src/notifier/tests/test_dispatcher.py +++ /dev/null @@ -1,15 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron 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 - - -class DispatchTestCase(TestCase): - # This is a stub, it will be filled out as this feature is remade with saner practices. - pass diff --git a/src/notifier/tests/test_models.py b/src/notifier/tests/test_models.py deleted file mode 100644 index d332254..0000000 --- a/src/notifier/tests/test_models.py +++ /dev/null @@ -1,30 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron 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 notifier.models import Notifier -from django.contrib.auth.models import User - - -class NotifierTestCase(TestCase): - - def test_valid_notifier_saves(self): - - sender = User.objects.create() - recipient = User.objects.create() - self.assertTrue( - Notifier.objects.create( - title='notification title', - content='notification body', - user=recipient, - sender=sender, - message_type='email' - ) - ) diff --git a/src/notifier/urls.py b/src/notifier/urls.py index 923cc33..f69ee85 100644 --- a/src/notifier/urls.py +++ b/src/notifier/urls.py @@ -5,15 +5,4 @@ # 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.urls import url - -from notifier.views import InboxView, NotificationView - -app_name = 'notifier' -urlpatterns = [ - url(r'^$', InboxView, name='messages'), - url(r'^notification/(?P<notification_id>[0-9]+)/$', NotificationView, name='notifier_single') -] +##############################################################################
\ No newline at end of file diff --git a/src/notifier/views.py b/src/notifier/views.py index 3a85eda..d65b13a 100644 --- a/src/notifier/views.py +++ b/src/notifier/views.py @@ -6,53 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - -from django.shortcuts import render -from notifier.models import Notification -from django.db.models import Q - - -def InboxView(request): - if request.user.is_authenticated: - user = request.user - else: - return render(request, "dashboard/login.html", - {'title': 'Authentication Required'}) - - return render(request, - "notifier/inbox.html", - {'unread_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(~Q(read_by=user.userprofile)), - 'read_notifications': Notification.objects.filter(recipients=user.userprofile).order_by('-id').filter(read_by=user.userprofile)}) - - -def NotificationView(request, notification_id): - - if request.user.is_authenticated: - user = request.user - else: - return render(request, - "dashboard/login.html", - {'title': 'Authentication Required'}) - - notification = Notification.objects.get(id=notification_id) - if user.userprofile not in notification.recipients.all(): - return render(request, - "dashboard/login.html", {'title': 'Access Denied'}) - - notification.read_by.add(user.userprofile) - notification.save() - if request.method == 'POST': - if 'delete' in request.POST: - # handle deleting - notification.recipients.remove(user.userprofile) - if not notification.recipients.exists(): - notification.delete() - else: - notification.save() - - if 'unread' in request.POST: - notification.read_by.remove(user.userprofile) - notification.save() - - return render(request, - "notifier/notification.html", {'notification': notification}) diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py index 2444a98..da9cba3 100644 --- a/src/resource_inventory/admin.py +++ b/src/resource_inventory/admin.py @@ -9,62 +9,3 @@ from django.contrib import admin - -from resource_inventory.forms import InterfaceConfigurationForm - -from resource_inventory.models import ( - ResourceProfile, - InterfaceProfile, - DiskProfile, - CpuProfile, - RamProfile, - ResourceTemplate, - ResourceConfiguration, - InterfaceConfiguration, - Server, - Interface, - Network, - Vlan, - ResourceBundle, - Scenario, - Installer, - Opsys, - OPNFVConfig, - OPNFVRole, - Image, - RemoteInfo, - PhysicalNetwork, - NetworkConnection, -) - - -admin.site.register([ - ResourceProfile, - InterfaceProfile, - DiskProfile, - CpuProfile, - RamProfile, - ResourceTemplate, - ResourceConfiguration, - Server, - Interface, - Network, - Vlan, - ResourceBundle, - Scenario, - Installer, - Opsys, - OPNFVConfig, - OPNFVRole, - Image, - PhysicalNetwork, - NetworkConnection, - RemoteInfo] -) - - -class InterfaceConfigurationAdmin(admin.ModelAdmin): - form = InterfaceConfigurationForm - - -admin.site.register(InterfaceConfiguration, InterfaceConfigurationAdmin) diff --git a/src/resource_inventory/forms.py b/src/resource_inventory/forms.py deleted file mode 100644 index fb8c102..0000000 --- a/src/resource_inventory/forms.py +++ /dev/null @@ -1,31 +0,0 @@ -############################################################################## -# Copyright (c) 2020 Sawyer Bergeron, 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.core.exceptions import ValidationError -from django import forms - -from resource_inventory.models import Network, InterfaceConfiguration - - -class InterfaceConfigurationForm(forms.ModelForm): - class Meta: - model = InterfaceConfiguration - fields = ['profile', 'resource_config', 'connections'] - - def clean(self): - connections = self.cleaned_data.get('connections') - resource_config = self.cleaned_data.get('resource_config') - - valid_nets = set(Network.objects.filter(bundle=resource_config.template)) - curr_nets = set([conn.network for conn in connections]) - - if not curr_nets.issubset(valid_nets): - raise ValidationError("Cannot have network connection to network outside pod") - - return self.cleaned_data diff --git a/src/resource_inventory/idf_templater.py b/src/resource_inventory/idf_templater.py deleted file mode 100644 index 8f0f924..0000000 --- a/src/resource_inventory/idf_templater.py +++ /dev/null @@ -1,148 +0,0 @@ -############################################################################## -# Copyright (c) 2019 Parker Berberian, Sawyer Bergeron, 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.loader import render_to_string - -from account.models import PublicNetwork - -from resource_inventory.models import Vlan - - -class IDFTemplater: - """Utility class to create a full Installer Descriptor File (IDF) yaml file.""" - - net_names = ["admin", "mgmt", "private", "public"] - bridge_names = { - "admin": "br-admin", - "mgmt": "br-mgmt", - "private": "br-private", - "public": "br-public" - } - - def __init__(self): - self.networks = {} - for i, name in enumerate(self.net_names): - self.networks[name] = { - "name": name, - "vlan": -1, - "interface": i, - "ip": "10.250." + str(i) + ".0", - "netmask": 24 - } - - def makeIDF(self, booking): - """Fill the IDF template with info about the resource.""" - template = "dashboard/idf.yaml" - info = {} - info['version'] = "0.1" - info['net_config'] = self.get_net_config(booking) - info['fuel'] = self.get_fuel_config(booking) - - return render_to_string(template, context=info) - - def get_net_config(self, booking): - net_config = {} - try: - net_config['oob'] = self.get_oob_net(booking) - except Exception: - net_config['oob'] = {} - try: - net_config['public'] = self.get_public_net(booking) - except Exception: - net_config['public'] = {} - - for net in [net for net in self.net_names if net != "public"]: - try: - net_config[net] = self.get_single_net_config(booking, net) - except Exception: - net_config[net] = {} - - return net_config - - def get_public_net(self, booking): - public = {} - config = booking.opnfv_config - public_role = config.networks.get(name="public") - public_vlan = Vlan.objects.filter(network=public_role.network).first() - public_network = PublicNetwork.objects.get(vlan=public_vlan.vlan_id, lab=booking.lab) - self.networks['public']['vlan'] = public_vlan.vlan_id - public['interface'] = self.networks['public']['interface'] - public['vlan'] = public_network.vlan # untagged?? - public['network'] = public_network.cidr.split("/")[0] - public['mask'] = public_network.cidr.split("/")[1] - # public['ip_range'] = 4 # necesary? - public['gateway'] = public_network.gateway - public['dns'] = ["1.1.1.1", "8.8.8.8"] - - return public - - def get_oob_net(self, booking): - net = {} - hosts = booking.resource.hosts.all() - addrs = [host.remote_management.address for host in hosts] - net['ip_range'] = ",".join(addrs) - net['vlan'] = "native" - return net - - def get_single_net_config(self, booking, net_name): - config = booking.opnfv_config - role = config.networks.get(name=net_name) - vlan = Vlan.objects.filter(network=role.network).first() - self.networks[net_name]['vlan'] = vlan.vlan_id - net = {} - net['interface'] = self.networks[net_name]['interface'] - net['vlan'] = vlan.vlan_id - net['network'] = self.networks[net_name]['ip'] - net['mask'] = self.networks[net_name]['netmask'] - - return net - - def get_fuel_config(self, booking): - fuel = {} - fuel['jumphost'] = {} - try: - fuel['jumphost']['bridges'] = self.get_fuel_bridges() - except Exception: - fuel['jumphost']['bridges'] = {} - - fuel['network'] = {} - try: - fuel['network']['nodes'] = self.get_fuel_nodes(booking) - except Exception: - fuel['network']['nodes'] = {} - - return fuel - - def get_fuel_bridges(self): - return self.bridge_names - - def get_fuel_nodes(self, booking): - jumphost = booking.opnfv_config.host_opnfv_config.get( - role__name__iexact="jumphost" - ) - hosts = booking.resource.hosts.exclude(pk=jumphost.pk) - nodes = [] - for host in hosts: - node = {} - ordered_interfaces = self.get_node_interfaces(host) - node['interfaces'] = [iface['name'] for iface in ordered_interfaces] - node['bus_addrs'] = [iface['bus'] for iface in ordered_interfaces] - nodes.append(node) - - return nodes - - def get_node_interfaces(self, node): - # TODO: this should sync with pdf ordering - interfaces = [] - - for iface in node.interfaces.all(): - interfaces.append({"name": iface.name, "bus": iface.bus_address}) - - return interfaces diff --git a/src/resource_inventory/migrations/0023_cloudinitfile_generated.py b/src/resource_inventory/migrations/0023_cloudinitfile_generated.py index b309753..eb10756 100644 --- a/src/resource_inventory/migrations/0023_cloudinitfile_generated.py +++ b/src/resource_inventory/migrations/0023_cloudinitfile_generated.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2 on 2021-12-17 18:54 +# Generated by Django 2.2 on 2023-06-07 19:48 from django.db import migrations, models diff --git a/src/resource_inventory/migrations/0024_auto_20230608_1913.py b/src/resource_inventory/migrations/0024_auto_20230608_1913.py new file mode 100644 index 0000000..1c8ea47 --- /dev/null +++ b/src/resource_inventory/migrations/0024_auto_20230608_1913.py @@ -0,0 +1,262 @@ +# Generated by Django 2.2 on 2023-06-08 19:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0023_auto_20230608_1913'), + ('booking', '0010_auto_20230608_1913'), + ('resource_inventory', '0023_cloudinitfile_generated'), + ] + + operations = [ + migrations.RemoveField( + model_name='cpuprofile', + name='host', + ), + migrations.RemoveField( + model_name='diskprofile', + name='host', + ), + migrations.RemoveField( + model_name='image', + name='from_lab', + ), + migrations.RemoveField( + model_name='image', + name='os', + ), + migrations.RemoveField( + model_name='image', + name='owner', + ), + migrations.RemoveField( + model_name='installer', + name='sup_scenarios', + ), + migrations.RemoveField( + model_name='interface', + name='acts_as', + ), + migrations.RemoveField( + model_name='interface', + name='config', + ), + migrations.RemoveField( + model_name='interface', + name='profile', + ), + migrations.RemoveField( + model_name='interfaceconfiguration', + name='connections', + ), + migrations.RemoveField( + model_name='interfaceconfiguration', + name='profile', + ), + migrations.RemoveField( + model_name='interfaceconfiguration', + name='resource_config', + ), + migrations.RemoveField( + model_name='interfaceprofile', + name='host', + ), + migrations.RemoveField( + model_name='network', + name='bundle', + ), + migrations.RemoveField( + model_name='networkconnection', + name='network', + ), + migrations.RemoveField( + model_name='networkrole', + name='network', + ), + migrations.RemoveField( + model_name='opnfvconfig', + name='installer', + ), + migrations.RemoveField( + model_name='opnfvconfig', + name='networks', + ), + migrations.RemoveField( + model_name='opnfvconfig', + name='scenario', + ), + migrations.RemoveField( + model_name='opnfvconfig', + name='template', + ), + migrations.RemoveField( + model_name='opsys', + name='from_lab', + ), + migrations.RemoveField( + model_name='physicalnetwork', + name='bundle', + ), + migrations.RemoveField( + model_name='physicalnetwork', + name='generic_network', + ), + migrations.RemoveField( + model_name='ramprofile', + name='host', + ), + migrations.RemoveField( + model_name='resourcebundle', + name='template', + ), + migrations.RemoveField( + model_name='resourceconfiguration', + name='cloud_init_files', + ), + migrations.RemoveField( + model_name='resourceconfiguration', + name='image', + ), + migrations.RemoveField( + model_name='resourceconfiguration', + name='profile', + ), + migrations.RemoveField( + model_name='resourceconfiguration', + name='template', + ), + migrations.RemoveField( + model_name='resourceopnfvconfig', + name='opnfv_config', + ), + migrations.RemoveField( + model_name='resourceopnfvconfig', + name='resource_config', + ), + migrations.RemoveField( + model_name='resourceopnfvconfig', + name='role', + ), + migrations.RemoveField( + model_name='resourceprofile', + name='labs', + ), + migrations.RemoveField( + model_name='resourcetemplate', + name='copy_of', + ), + migrations.RemoveField( + model_name='resourcetemplate', + name='lab', + ), + migrations.RemoveField( + model_name='resourcetemplate', + name='owner', + ), + migrations.RemoveField( + model_name='server', + name='bundle', + ), + migrations.RemoveField( + model_name='server', + name='config', + ), + migrations.RemoveField( + model_name='server', + name='interfaces', + ), + migrations.RemoveField( + model_name='server', + name='lab', + ), + migrations.RemoveField( + model_name='server', + name='profile', + ), + migrations.RemoveField( + model_name='server', + name='remote_management', + ), + migrations.RemoveField( + model_name='vlan', + name='network', + ), + migrations.DeleteModel( + name='CloudInitFile', + ), + migrations.DeleteModel( + name='CpuProfile', + ), + migrations.DeleteModel( + name='DiskProfile', + ), + migrations.DeleteModel( + name='Image', + ), + migrations.DeleteModel( + name='Installer', + ), + migrations.DeleteModel( + name='Interface', + ), + migrations.DeleteModel( + name='InterfaceConfiguration', + ), + migrations.DeleteModel( + name='InterfaceProfile', + ), + migrations.DeleteModel( + name='Network', + ), + migrations.DeleteModel( + name='NetworkConnection', + ), + migrations.DeleteModel( + name='NetworkRole', + ), + migrations.DeleteModel( + name='OPNFVConfig', + ), + migrations.DeleteModel( + name='OPNFVRole', + ), + migrations.DeleteModel( + name='Opsys', + ), + migrations.DeleteModel( + name='PhysicalNetwork', + ), + migrations.DeleteModel( + name='RamProfile', + ), + migrations.DeleteModel( + name='RemoteInfo', + ), + migrations.DeleteModel( + name='ResourceBundle', + ), + migrations.DeleteModel( + name='ResourceConfiguration', + ), + migrations.DeleteModel( + name='ResourceOPNFVConfig', + ), + migrations.DeleteModel( + name='ResourceProfile', + ), + migrations.DeleteModel( + name='ResourceTemplate', + ), + migrations.DeleteModel( + name='Scenario', + ), + migrations.DeleteModel( + name='Server', + ), + migrations.DeleteModel( + name='Vlan', + ), + ] diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index 5d87430..a143cfd 100644 --- a/src/resource_inventory/models.py +++ b/src/resource_inventory/models.py @@ -6,6 +6,14 @@ # 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 +############################################################################################################################################################ +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# Copyright (c) 2020 Sawyer Bergeron, Sean Smith, 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.auth.models import User @@ -22,684 +30,9 @@ from collections import Counter from account.models import Lab from dashboard.utils import AbstractModelQuery -""" -Profiles of resources hosted by labs. - -These describe hardware attributes of the different Resources a lab hosts. -A single Resource subclass (e.g. Server) may have instances that point to different -Profile models (e.g. an x86 server profile and armv8 server profile. -""" - - -class ResourceProfile(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200, unique=True) - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64") - ]) - description = models.TextField() - labs = models.ManyToManyField(Lab, related_name="resourceprofiles") - - def validate(self): - validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$") - if not validname.match(self.name): - return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces." - else: - return None - - def __str__(self): - return self.name - - def get_resources(self, lab=None, working=True, unreserved=False): - """ - Return a list of Resource objects which have this profile. - - If lab is provided, only resources at that lab will be returned. - If working=True, will only return working hosts - """ - resources = [] - query = Q(profile=self) - if lab: - query = query & Q(lab=lab) - if working: - query = query & Q(working=True) - - resources = ResourceQuery.filter(query) - - if unreserved: - resources = [r for r in resources if not r.is_reserved()] - - return resources - - -class InterfaceProfile(models.Model): - id = models.AutoField(primary_key=True) - speed = models.IntegerField() - name = models.CharField(max_length=100) - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile') - nic_type = models.CharField( - max_length=50, - choices=[ - ("onboard", "onboard"), - ("pcie", "pcie") - ], - default="onboard" - ) - order = models.IntegerField(default=-1) - - def __str__(self): - return self.name + " for " + str(self.host) - - -class DiskProfile(models.Model): - id = models.AutoField(primary_key=True) - size = models.IntegerField() - media_type = models.CharField(max_length=50, choices=[ - ("SSD", "SSD"), - ("HDD", "HDD") - ]) - name = models.CharField(max_length=50) - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile') - rotation = models.IntegerField(default=0) - interface = models.CharField( - max_length=50, - choices=[ - ("sata", "sata"), - ("sas", "sas"), - ("ssd", "ssd"), - ("nvme", "nvme"), - ("scsi", "scsi"), - ("iscsi", "iscsi"), - ], - default="sata" - ) - - def __str__(self): - return self.name + " for " + str(self.host) - - -class CpuProfile(models.Model): - id = models.AutoField(primary_key=True) - cores = models.IntegerField() - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64") - ]) - cpus = models.IntegerField() - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile') - cflags = models.TextField(null=True, blank=True) - - def __str__(self): - return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host) - - -class RamProfile(models.Model): - id = models.AutoField(primary_key=True) - amount = models.IntegerField() - channels = models.IntegerField() - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='ramprofile') - - def __str__(self): - return str(self.amount) + "G for " + str(self.host) - - -""" -Resource Models - -These models represent actual hardware resources -with varying degrees of abstraction. -""" - - -class CloudInitFile(models.Model): - text = models.TextField() - - # higher priority is applied later, so "on top" of existing files - priority = models.IntegerField() - generated = models.BooleanField(default=False) - - @classmethod - def merge_strategy(cls): - return [ - {'name': 'list', 'settings': ['append']}, - {'name': 'dict', 'settings': ['recurse_list', 'replace']}, - ] - - @classmethod - def create(cls, text="", priority=0): - return CloudInitFile.objects.create(priority=priority, text=text) - - -class ResourceTemplate(models.Model): - """ - Models a "template" of a complete, configured collection of resources that can be booked. - - For example, this may represent a Pharos POD. This model is a template of the actual - resources that will be booked. This model can be "instantiated" into real resource models - across multiple different bookings. - """ - - # TODO: template might not be a good name because this is a collection of lots of configured resources - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) - xml = models.TextField() - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL, related_name="resourcetemplates") - description = models.CharField(max_length=1000, default="") - public = models.BooleanField(default=False) - temporary = models.BooleanField(default=False) - copy_of = models.ForeignKey("ResourceTemplate", blank=True, null=True, on_delete=models.SET_NULL) - - # if these fields are empty ("") then they are implicitly "every vlan", - # otherwise we filter any allocations we try to instantiate against this list - # they should be represented as a json list of integers - private_vlan_pool = models.TextField(default="") - public_vlan_pool = models.TextField(default="") - - def private_vlan_pool_set(self): - if self.private_vlan_pool != "": - return set(json.loads(self.private_vlan_pool)) - else: - return None - - def public_vlan_pool_set(self): - if self.private_vlan_pool != "": - return set(json.loads(self.public_vlan_pool)) - else: - return None - - def getConfigs(self): - configs = self.resourceConfigurations.all() - return list(configs) - - def get_required_resources(self): - profiles = Counter([str(config.profile) for config in self.getConfigs()]) - return dict(profiles) - - def __str__(self): - return self.name - - -class ResourceBundle(models.Model): - """ - Collection of Resource objects. - - This is just a way of aggregating all the resources in a booking into a single model. - """ - - template = models.ForeignKey(ResourceTemplate, on_delete=models.SET_NULL, null=True) - - def __str__(self): - if self.template is None: - return "Resource bundle " + str(self.id) + " with no template" - return "instance of " + str(self.template) - - def get_resources(self): - return ResourceQuery.filter(bundle=self) - - def get_resource_with_role(self, role): - # TODO - pass - - def release(self): - for pn in PhysicalNetwork.objects.filter(bundle=self).all(): - try: - pn.release() - except Exception as e: - print("Exception occurred while trying to release resource ", pn.vlan_id) - print(e) - traceback.print_exc() - - for resource in self.get_resources(): - try: - resource.release() - except Exception as e: - print("Exception occurred while trying to release resource ", resource) - print(e) - traceback.print_exc() - - def get_template_name(self): - if not self.template: - return "" - if not self.template.temporary: - return self.template.name - return self.template.copy_of.name - - -class ResourceConfiguration(models.Model): - """Model to represent a complete configuration for a single physical Resource.""" - - id = models.AutoField(primary_key=True) - profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) - image = models.ForeignKey("Image", on_delete=models.PROTECT) - template = models.ForeignKey(ResourceTemplate, related_name="resourceConfigurations", null=True, on_delete=models.CASCADE) - is_head_node = models.BooleanField(default=False) - name = models.CharField(max_length=3000, default="opnfv_host") - - cloud_init_files = models.ManyToManyField(CloudInitFile, blank=True) - - def __str__(self): - return str(self.name) - - def ci_file_list(self): - return list(self.cloud_init_files.order_by("priority").all()) - - +# Keep for now until migrations are made, otherwise django will get angry def get_default_remote_info(): - return RemoteInfo.objects.get_or_create( - address="default", - mac_address="default", - password="default", - user="default", - management_type="default", - versions="[default]" - )[0].pk - - -class Resource(models.Model): - """ - Super class for all hardware resource models. - - Defines methods that must be implemented and common database fields. - Any new kind of Resource a lab wants to host (White box switch, traffic generator, etc) - should inherit from this class and fulfill the functional interface - """ - - class Meta: - abstract = True - - bundle = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, blank=True, null=True) - profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) - config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, blank=True, null=True) - working = models.BooleanField(default=True) - vendor = models.CharField(max_length=100, default="unknown") - model = models.CharField(max_length=150, default="unknown") - interfaces = models.ManyToManyField("Interface") - remote_management = models.ForeignKey("RemoteInfo", default=get_default_remote_info, on_delete=models.SET(get_default_remote_info)) - labid = models.CharField(max_length=200, default="default_id", unique=True) - lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - - def get_configuration(self, state): - """ - Get configuration of Resource. - - Returns the desired configuration for this host as a - JSON object as defined in the rest api spec. - state is a ConfigState - """ - raise NotImplementedError("Must implement in concrete Resource classes") - - def reserve(self): - """Reserve this resource for its currently assigned booking.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def release(self): - """Make this resource available again for new boookings.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def get_interfaces(self): - """ - Return a list of interfaces on this resource. - - The ordering of interfaces should be consistent. - """ - raise NotImplementedError("Must implement in concrete Resource classes") - - def is_reserved(self): - """Return True if this Resource is reserved.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def same_instance(self, other): - """Return True if this Resource is the same instance as other.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def save(self, *args, **kwargs): - """Assert that labid is unique across all Resource models.""" - res = ResourceQuery.filter(labid=self.labid) - if len(res) > 1: - raise ValidationError("Too many resources with labid " + str(self.labid)) - - if len(res) == 1: - if not self.same_instance(res[0]): - raise ValidationError("Too many resources with labid " + str(self.labid)) - super().save(*args, **kwargs) - - -class RemoteInfo(models.Model): - address = models.CharField(max_length=15) - mac_address = models.CharField(max_length=17) - password = models.CharField(max_length=100) - user = models.CharField(max_length=100) - management_type = models.CharField(max_length=50, default="ipmi") - versions = models.CharField(max_length=100) # json serialized list of floats - - -class Server(Resource): - """Resource subclass - a basic baremetal server.""" - - booked = models.BooleanField(default=False) - name = models.CharField(max_length=200, unique=True) - - def __str__(self): - return self.name - - def get_configuration(self, state): - ipmi = state == ConfigState.NEW - power = "off" if state == ConfigState.CLEAN else "on" - image = self.config.image.lab_id if self.config else "unknown" - - return { - "id": self.labid, - "image": image, - "hostname": self.config.name, - "power": power, - "ipmi_create": str(ipmi) - } - - def get_interfaces(self): - return list(self.interfaces.all().order_by('bus_address')) - - def release(self): - self.bundle = None - self.booked = False - self.save() - - def reserve(self): - self.booked = True - self.save() - - def is_reserved(self): - return self.booked - - def same_instance(self, other): - return isinstance(other, Server) and other.name == self.name - - -def is_serializable(data): - try: - json.dumps(data) - return True - except Exception: - return False - - -class Opsys(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - lab_id = models.CharField(max_length=100) - obsolete = models.BooleanField(default=False) - available = models.BooleanField(default=True) # marked true by Cobbler if it exists there - from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - - indexes = [ - models.Index(fields=['cobbler_id']) - ] - - def new_from_data(data): - opsys = Opsys() - opsys.update(data) - return opsys - - def serialize(self): - d = {} - for field in vars(self): - attr = getattr(self, field) - if is_serializable(attr): - d[field] = attr - return d - - def update(self, data): - for field in vars(self): - if field in data: - setattr(self, field, data[field] if data[field] else getattr(self, field)) - - def __str__(self): - return self.name - - -class Image(models.Model): - """Model for representing OS images / snapshots of hosts.""" - - id = models.AutoField(primary_key=True) - from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64"), - ("unknown", "unknown"), - ]) - lab_id = models.CharField(max_length=100) - name = models.CharField(max_length=100) - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - public = models.BooleanField(default=True) - description = models.TextField() - os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) - - available = models.BooleanField(default=True) # marked True by cobbler if it exists there - obsolete = models.BooleanField(default=False) - - indexes = [ - models.Index(fields=['architecture']), - models.Index(fields=['cobbler_id']) - ] - - def __str__(self): - return self.name - - def is_obsolete(self): - return self.obsolete or self.os.obsolete - - def serialize(self): - d = {} - for field in vars(self): - attr = getattr(self, field) - if is_serializable(attr): - d[field] = attr - return d - - def update(self, data): - for field in vars(self): - if field in data: - setattr(self, field, data[field] if data[field] else getattr(self, field)) - - def new_from_data(data): - img = Image() - img.update(data) - return img - - def in_use(self): - for resource in ResourceQuery.filter(config__image=self): - if resource.is_reserved(): - return True - - return False - - -""" -Networking configuration models -""" - - -class Network(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200) - bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks") - is_public = models.BooleanField() - - def __str__(self): - return self.name - - -class PhysicalNetwork(models.Model): - vlan_id = models.IntegerField() - generic_network = models.ForeignKey(Network, on_delete=models.CASCADE) - bundle = models.ForeignKey(ResourceBundle, null=True, blank=True, on_delete=models.CASCADE) - - def get_configuration(self, state): - """ - Get the network configuration. - - Collects info about each attached network interface and vlan, etc - """ - return {} - - def reserve(self): - """Reserve vlan(s) associated with this network.""" - return False - - def release(self): - from booking.models import Booking - - booking = Booking.objects.get(resource=self.bundle) - lab = booking.lab - vlan_manager = lab.vlan_manager - - if self.generic_network.is_public: - vlan_manager.release_public_vlan(self.vlan_id) - else: - vlan_manager.release_vlans([self.vlan_id]) - return False - - def __str__(self): - return 'Physical Network for ' + self.generic_network.name - - -class NetworkConnection(models.Model): - network = models.ForeignKey(Network, on_delete=models.CASCADE) - vlan_is_tagged = models.BooleanField() - - def __str__(self): - return 'Connection to ' + self.network.name - - -class Vlan(models.Model): - id = models.AutoField(primary_key=True) - vlan_id = models.IntegerField() - tagged = models.BooleanField() - public = models.BooleanField(default=False) - network = models.ForeignKey(PhysicalNetwork, on_delete=models.DO_NOTHING, null=True) - - def __str__(self): - return str(self.vlan_id) + ("_T" if self.tagged else "") - - -class InterfaceConfiguration(models.Model): - id = models.AutoField(primary_key=True) - profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) - resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs') - connections = models.ManyToManyField(NetworkConnection, blank=True) - - def __str__(self): - return "type " + str(self.profile) + " on host " + str(self.resource_config) - - -""" -OPNFV / Software configuration models -""" - - -class Scenario(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) - - def __str__(self): - return self.name - - -class Installer(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200) - sup_scenarios = models.ManyToManyField(Scenario, blank=True) - - def __str__(self): - return self.name - - -class NetworkRole(models.Model): - name = models.CharField(max_length=100) - network = models.ForeignKey(Network, on_delete=models.CASCADE) - - -def create_resource_ref_string(for_hosts: [str]) -> str: - # need to sort the list, then do dump - for_hosts.sort() - - return json.dumps(for_hosts) - - -class OPNFVConfig(models.Model): - id = models.AutoField(primary_key=True) - installer = models.ForeignKey(Installer, on_delete=models.CASCADE) - scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE) - template = models.ForeignKey(ResourceTemplate, related_name="opnfv_config", on_delete=models.CASCADE) - networks = models.ManyToManyField(NetworkRole) - name = models.CharField(max_length=300, blank=True, default="") - description = models.CharField(max_length=600, blank=True, default="") - - def __str__(self): - return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario) - - -class OPNFVRole(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200) - description = models.TextField() - - def __str__(self): - return self.name - + pass def get_sentinal_opnfv_role(): - return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.") - - -class ResourceOPNFVConfig(models.Model): - role = models.ForeignKey(OPNFVRole, related_name="resource_opnfv_configs", on_delete=models.CASCADE) - resource_config = models.ForeignKey(ResourceConfiguration, related_name="resource_opnfv_config", on_delete=models.CASCADE) - opnfv_config = models.ForeignKey(OPNFVConfig, related_name="resource_opnfv_config", on_delete=models.CASCADE) - - -class Interface(models.Model): - id = models.AutoField(primary_key=True) - mac_address = models.CharField(max_length=17) - bus_address = models.CharField(max_length=50) - config = models.ManyToManyField(Vlan) - acts_as = models.OneToOneField(InterfaceConfiguration, blank=True, null=True, on_delete=models.CASCADE) - profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) - - def __str__(self): - return self.mac_address + " on host " + str(self.profile.host.name) - - def clean(self, *args, **kwargs): - if self.acts_as and self.acts_as.profile != self.profile: - raise ValidationError("Interface Configuration's Interface Profile does not match Interface Profile chosen for Interface.") - super().clean(*args, **kwargs) - - def save(self, *args, **kwargs): - self.full_clean() - super().save(*args, **kwargs) - - -""" -Some Enums for dealing with global constants. -""" - - -class OPNFV_SETTINGS(): - """This is a static configuration class.""" - - # all the required network types in PDF/IDF spec - NETWORK_ROLES = ["public", "private", "admin", "mgmt"] - - -class ConfigState: - NEW = 0 - RESET = 100 - CLEAN = 200 - - -RESOURCE_TYPES = [Server] - - -class ResourceQuery(AbstractModelQuery): - model_list = [Server] + pass diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py deleted file mode 100644 index c4b22fe..0000000 --- a/src/resource_inventory/pdf_templater.py +++ /dev/null @@ -1,176 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.loader import render_to_string -import booking -from resource_inventory.models import Server - - -class PDFTemplater: - """Utility class to create a full PDF yaml file.""" - - @classmethod - def makePDF(cls, booking): - """Fill the pod descriptor file template with info about the resource.""" - template = "dashboard/pdf.yaml" - info = {} - info['details'] = cls.get_pdf_details(booking.resource) - 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) - - @classmethod - def get_pdf_details(cls, resource): - """Info for the "details" section.""" - details = {} - owner = "Anon" - email = "email@mail.com" - resource_lab = resource.template.lab - lab = resource_lab.name - location = resource_lab.location - pod_type = "development" - link = resource_lab.lab_info_link - - try: - # try to get more specific info that may fail, we dont care if it does - booking_owner = booking.models.Booking.objects.get(resource=resource).owner - owner = booking_owner.username - email = booking_owner.userprofile.email_addr - except Exception: - pass - - details['contact'] = email - details['lab'] = lab - details['link'] = link - details['owner'] = owner - details['location'] = location - details['type'] = pod_type - - return details - - @classmethod - def get_jumphost(cls, booking): - """Return the host designated as the Jumphost for the booking.""" - jumphost = None - if booking.opnfv_config: - jumphost_opnfv_config = booking.opnfv_config.host_opnfv_config.get( - role__name__iexact="jumphost" - ) - jumphost = booking.resource.hosts.get(config=jumphost_opnfv_config.host_config) - else: # if there is no opnfv config, use headnode - jumphost = Server.objects.filter( - bundle=booking.resource, - config__is_head_node=True - ).first() - - return jumphost - - @classmethod - def get_pdf_jumphost(cls, booking): - """Return a dict of all the info for the "jumphost" section.""" - jumphost = cls.get_jumphost(booking) - jumphost_info = cls.get_pdf_host(jumphost) - jumphost_info['os'] = jumphost.config.image.os.name - return jumphost_info - - @classmethod - def get_pdf_nodes(cls, booking): - """Return a list of all the "nodes" (every host except jumphost).""" - pdf_nodes = [] - nodes = set(Server.objects.filter(bundle=booking.resource)) - nodes.discard(cls.get_jumphost(booking)) - - for node in nodes: - pdf_nodes.append(cls.get_pdf_host(node)) - - return pdf_nodes - - @classmethod - def get_pdf_host(cls, host): - """ - Gather all needed info about a host. - - returns a dictionary - """ - host_info = {} - host_info['name'] = host.name - host_info['node'] = cls.get_pdf_host_node(host) - host_info['disks'] = [] - for disk in host.profile.storageprofile.all(): - host_info['disks'].append(cls.get_pdf_host_disk(disk)) - - host_info['interfaces'] = [] - for interface in host.interfaces.all(): - host_info['interfaces'].append(cls.get_pdf_host_iface(interface)) - - host_info['remote'] = cls.get_pdf_host_remote_management(host) - - return host_info - - @classmethod - def get_pdf_host_node(cls, host): - """Return "node" info for a given host.""" - d = {} - d['type'] = "baremetal" - d['vendor'] = host.vendor - d['model'] = host.model - d['memory'] = str(host.profile.ramprofile.first().amount) + "G" - - cpu = host.profile.cpuprofile.first() - d['arch'] = cpu.architecture - d['cpus'] = cpu.cpus - d['cores'] = cpu.cores - cflags = cpu.cflags - if cflags and cflags.strip(): - d['cpu_cflags'] = cflags - else: - d['cpu_cflags'] = "none" - - return d - - @classmethod - def get_pdf_host_disk(cls, disk): - """Return a dict describing the given disk.""" - disk_info = {} - disk_info['name'] = disk.name - disk_info['capacity'] = str(disk.size) + "G" - disk_info['type'] = disk.media_type - disk_info['interface'] = disk.interface - disk_info['rotation'] = disk.rotation - return disk_info - - @classmethod - def get_pdf_host_iface(cls, interface): - """Return a dict describing given interface.""" - iface_info = {} - iface_info['features'] = "none" - iface_info['mac_address'] = interface.mac_address - iface_info['name'] = interface.profile.name - speed = str(int(interface.profile.speed / 1000)) + "gb" - iface_info['speed'] = speed - return iface_info - - @classmethod - def get_pdf_host_remote_management(cls, host): - """Get the remote params of the host.""" - man = host.remote_management - mgmt = {} - mgmt['address'] = man.address - mgmt['mac_address'] = man.mac_address - mgmt['pass'] = man.password - mgmt['type'] = man.management_type - mgmt['user'] = man.user - mgmt['versions'] = [man.versions] - return mgmt diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py deleted file mode 100644 index 16c106e..0000000 --- a/src/resource_inventory/resource_manager.py +++ /dev/null @@ -1,197 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 __future__ import annotations # noqa: F407 - -import re -from typing import Optional -from django.db.models import Q - -from dashboard.exceptions import ResourceAvailabilityException - -from resource_inventory.models import ( - Resource, - ResourceBundle, - ResourceTemplate, - ResourceConfiguration, - Network, - Vlan, - PhysicalNetwork, - InterfaceConfiguration, -) - -from account.models import Lab -from django.contrib.auth.models import User - - -class ResourceManager: - - instance = None - - def __init__(self): - pass - - @staticmethod - def getInstance() -> ResourceManager: - if ResourceManager.instance is None: - ResourceManager.instance = ResourceManager() - return ResourceManager.instance - - def getAvailableResourceTemplates(self, lab: Lab, user: Optional[User] = None) -> list[ResourceTemplate]: - filter = Q(public=True) - if user: - filter = filter | Q(owner=user) - filter = filter & Q(temporary=False) & Q(lab=lab) - return ResourceTemplate.objects.filter(filter) - - def templateIsReservable(self, resource_template: ResourceTemplate): - """ - Check if the required resources to reserve this template is available. - - No changes to the database - """ - # count up hosts - profile_count = {} - for config in resource_template.getConfigs(): - if config.profile not in profile_count: - profile_count[config.profile] = 0 - profile_count[config.profile] += 1 - - # check that all required hosts are available - for profile in profile_count.keys(): - available = len(profile.get_resources(lab=resource_template.lab, unreserved=True)) - needed = profile_count[profile] - if available < needed: - return False - return True - - # public interface - def deleteResourceBundle(self, resourceBundle: ResourceBundle): - raise NotImplementedError("Resource Bundle Deletion Not Implemented") - - def releaseResourceBundle(self, resourceBundle: ResourceBundle): - resourceBundle.release() - - def get_vlans(self, resourceTemplate: ResourceTemplate) -> dict[str, int]: - """ - returns: dict from network name to the associated vlan number (backend vlan id) - """ - networks = {} - vlan_manager = resourceTemplate.lab.vlan_manager - for network in resourceTemplate.networks.all(): - if network.is_public: - # already throws if can't get requested count, so can always expect public_net to be Some - public_net = vlan_manager.get_public_vlan(within=resourceTemplate.public_vlan_pool_set()) - vlan_manager.reserve_public_vlan(public_net.vlan) - networks[network.name] = public_net.vlan - else: - # already throws if can't get requested count, so can always index in @ 0 - vlans = vlan_manager.get_vlans(count=1, within=resourceTemplate.private_vlan_pool_set()) - vlan_manager.reserve_vlans(vlans[0]) - networks[network.name] = vlans[0] - return networks - - def instantiateTemplate(self, resource_template: ResourceTemplate): - """ - Convert a ResourceTemplate into a ResourceBundle. - - Takes in a ResourceTemplate and reserves all the - Resources needed and returns a completed ResourceBundle. - """ - resource_bundle = ResourceBundle.objects.create(template=resource_template) - res_configs = resource_template.getConfigs() - resources = [] - - vlan_map = self.get_vlans(resource_template) - - for config in res_configs: - try: - phys_res = self.acquireHost(config) - phys_res.bundle = resource_bundle - phys_res.config = config - resources.append(phys_res) - - self.configureNetworking(resource_bundle, phys_res, vlan_map) - phys_res.save() - - except Exception as e: - self.fail_acquire(resources, vlan_map, resource_template) - raise e - - return resource_bundle - - def configureNetworking(self, resource_bundle: ResourceBundle, resource: Resource, vlan_map: dict[str, int]): - """ - @vlan_map: dict from network name to the associated vlan number (backend vlan id) - """ - for physical_interface in resource.interfaces.all(): - - # assign interface configs - iface_config = InterfaceConfiguration.objects.get( - profile=physical_interface.profile, - resource_config=resource.config - ) - - physical_interface.acts_as = iface_config - physical_interface.acts_as.save() - - physical_interface.config.clear() - for connection in iface_config.connections.all(): - physicalNetwork = PhysicalNetwork.objects.create( - vlan_id=vlan_map[connection.network.name], - generic_network=connection.network, - bundle=resource_bundle, - ) - physical_interface.config.add( - Vlan.objects.create( - vlan_id=vlan_map[connection.network.name], - tagged=connection.vlan_is_tagged, - public=connection.network.is_public, - network=physicalNetwork - ) - ) - - # private interface - def acquireHost(self, resource_config: ResourceConfiguration) -> Resource: - resources = resource_config.profile.get_resources( - lab=resource_config.template.lab, - unreserved=True - ) - - try: - resource = resources[0] # TODO: should we randomize and 'load balance' the servers? - resource.config = resource_config - resource.reserve() - return resource - except IndexError: - raise ResourceAvailabilityException("No available resources of requested type") - - def releaseNetworks(self, template, vlans): - vlan_manager = template.lab.vlan_manager - for net_name, vlan_id in vlans.items(): - net = Network.objects.get(name=net_name, bundle=template) - if (net.is_public): - vlan_manager.release_public_vlan(vlan_id) - else: - vlan_manager.release_vlans(vlan_id) - - def fail_acquire(self, hosts, vlans, template): - self.releaseNetworks(template, vlans) - for host in hosts: - host.release() - - -class HostNameValidator(object): - regex = r'^[A-Za-z0-9][A-Za-z0-9-]*$' - message = "Hostnames can only contain alphanumeric characters and hyphens (-). Hostnames must start with a letter" - pattern = re.compile(regex) - - @classmethod - def is_valid_hostname(cls, hostname): - return len(hostname) < 65 and cls.pattern.fullmatch(hostname) is not None diff --git a/src/resource_inventory/tests/test_managers.py b/src/resource_inventory/tests/test_managers.py deleted file mode 100644 index 46cee5a..0000000 --- a/src/resource_inventory/tests/test_managers.py +++ /dev/null @@ -1,301 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.contrib.auth.models import User - -from resource.inventory_manager import InventoryManager -from resource.resource_manager import ResourceManager, HostNameValidator -from account.models import Lab -from resource.models import ( - Host, - Vlan, - Interface, - ResourceBundle, - GenericHost, - GenericResourceBundle, - CpuProfile, - RamProfile, - DiskProfile, - HostProfile, - InterfaceProfile -) - - -class InventoryManagerTestCase(TestCase): - - def test_singleton(self): - instance = InventoryManager.getInstance() - self.assertTrue(isinstance(instance, InventoryManager)) - self.assertTrue(instance is InventoryManager.getInstance()) - - def setUp(self): - # setup - # create lab and give it resources - user = User.objects.create(username="username") - self.lab = Lab.objects.create( - lab_user=user, - name='test lab', - contact_email='someone@email.com', - contact_phone='dont call me' - ) - - # create hostProfile - hostProfile = HostProfile.objects.create( - host_type=0, - name='Test profile', - description='a test profile' - ) - InterfaceProfile.objects.create( - speed=1000, - name='eno3', - host=hostProfile - ) - DiskProfile.objects.create( - size=1000, - media_type="SSD", - name='/dev/sda', - host=hostProfile - ) - CpuProfile.objects.create( - cores=96, - architecture="x86_64", - cpus=2, - host=hostProfile - ) - RamProfile.objects.create( - amount=256, - channels=4, - host=hostProfile - ) - - # create GenericResourceBundle - genericBundle = GenericResourceBundle.objects.create() - - self.gHost1 = GenericHost.objects.create( - bundle=genericBundle, - name='generic host 1', - profile=hostProfile - ) - self.gHost2 = GenericHost.objects.create( - bundle=genericBundle, - name='generic host 2', - profile=hostProfile - ) - - # actual resource bundle - bundle = ResourceBundle.objects.create(template=genericBundle) - - self.host1 = Host.objects.create( - template=self.gHost1, - booked=True, - name='host1', - bundle=bundle, - profile=hostProfile, - lab=self.lab - ) - - self.host2 = Host.objects.create( - template=self.gHost2, - booked=True, - name='host2', - bundle=bundle, - profile=hostProfile, - lab=self.lab - ) - - vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) - vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) - - Interface.objects.create( - mac_address='00:11:22:33:44:55', - bus_address='some bus address', - switch_name='switch1', - port_name='port10', - config=vlan1, - host=self.host1 - ) - Interface.objects.create( - mac_address='00:11:22:33:44:56', - bus_address='some bus address', - switch_name='switch1', - port_name='port12', - config=vlan2, - host=self.host2 - ) - - def test_acquire_host(self): - host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) - self.assertNotEquals(host, None) - self.assertTrue(host.booked) - self.assertEqual(host.template, self.gHost1) - - def test_release_host(self): - host = InventoryManager.getInstance().acquireHost(self.gHost1, self.lab.name) - self.assertTrue(host.booked) - InventoryManager.getInstance().releaseHost(host) - self.assertFalse(host.booked) - - -class ResourceManagerTestCase(TestCase): - def test_singleton(self): - instance = ResourceManager.getInstance() - self.assertTrue(isinstance(instance, ResourceManager)) - self.assertTrue(instance is ResourceManager.getInstance()) - - def setUp(self): - # setup - # create lab and give it resources - user = User.objects.create(username="username") - self.lab = Lab.objects.create( - lab_user=user, - name='test lab', - contact_email='someone@email.com', - contact_phone='dont call me' - ) - - # create hostProfile - hostProfile = HostProfile.objects.create( - host_type=0, - name='Test profile', - description='a test profile' - ) - InterfaceProfile.objects.create( - speed=1000, - name='eno3', - host=hostProfile - ) - DiskProfile.objects.create( - size=1000, - media_type="SSD", - name='/dev/sda', - host=hostProfile - ) - CpuProfile.objects.create( - cores=96, - architecture="x86_64", - cpus=2, - host=hostProfile - ) - RamProfile.objects.create( - amount=256, - channels=4, - host=hostProfile - ) - - # create GenericResourceBundle - genericBundle = GenericResourceBundle.objects.create() - - self.gHost1 = GenericHost.objects.create( - bundle=genericBundle, - name='generic host 1', - profile=hostProfile - ) - self.gHost2 = GenericHost.objects.create( - bundle=genericBundle, - name='generic host 2', - profile=hostProfile - ) - - # actual resource bundle - bundle = ResourceBundle.objects.create(template=genericBundle) - - self.host1 = Host.objects.create( - template=self.gHost1, - booked=True, - name='host1', - bundle=bundle, - profile=hostProfile, - lab=self.lab - ) - - self.host2 = Host.objects.create( - template=self.gHost2, - booked=True, - name='host2', - bundle=bundle, - profile=hostProfile, - lab=self.lab - ) - - vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) - vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) - - Interface.objects.create( - mac_address='00:11:22:33:44:55', - bus_address='some bus address', - switch_name='switch1', - port_name='port10', - config=vlan1, - host=self.host1 - ) - Interface.objects.create( - mac_address='00:11:22:33:44:56', - bus_address='some bus address', - switch_name='switch1', - port_name='port12', - config=vlan2, - host=self.host2 - ) - - def test_convert_bundle(self): - ResourceManager.getInstance().convertResoureBundle(self.genericBundle, self.lab.name) - # verify bundle configuration - - -class HostNameValidatorTestCase(TestCase): - - def test_valid_hostnames(self): - self.assertTrue(HostNameValidator.is_valid_hostname("localhost")) - self.assertTrue(HostNameValidator.is_valid_hostname("Localhost")) - self.assertTrue(HostNameValidator.is_valid_hostname("localHost")) - self.assertTrue(HostNameValidator.is_valid_hostname("LOCALHOST")) - self.assertTrue(HostNameValidator.is_valid_hostname("f")) - self.assertTrue(HostNameValidator.is_valid_hostname("abc123doreyme")) - self.assertTrue(HostNameValidator.is_valid_hostname("F9999999")) - self.assertTrue(HostNameValidator.is_valid_hostname("my-host")) - self.assertTrue(HostNameValidator.is_valid_hostname("My-Host")) - self.assertTrue(HostNameValidator.is_valid_hostname("MY-HOST")) - self.assertTrue(HostNameValidator.is_valid_hostname("a-long-name-for-my-host")) - - def test_invalid_hostnames(self): - self.assertFalse(HostNameValidator.is_valid_hostname("-long-name-for-my-host")) - self.assertFalse(HostNameValidator.is_valid_hostname("546")) - self.assertFalse(HostNameValidator.is_valid_hostname("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) - - def test_invalid_chars(self): - self.assertFalse(HostNameValidator.is_valid_hostname("contains!char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains@char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains#char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains$char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains%char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains^char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains&char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains*char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains(char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains)char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains_char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains=char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains+char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains|char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains\\char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains[char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains]char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains;char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains:char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains'char")) - self.assertFalse(HostNameValidator.is_valid_hostname('contains"char')) - self.assertFalse(HostNameValidator.is_valid_hostname("contains'char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains<char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains>char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains,char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains?char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains/char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains`char")) - self.assertFalse(HostNameValidator.is_valid_hostname("contains~char")) diff --git a/src/resource_inventory/tests/test_models.py b/src/resource_inventory/tests/test_models.py deleted file mode 100644 index 3f2d1d8..0000000 --- a/src/resource_inventory/tests/test_models.py +++ /dev/null @@ -1,173 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.contrib.auth.models import User -from account.models import Lab -from resource_inventory.models import ( - Scenario, - Installer, - Opsys, - ConfigBundle, - OPNFVConfig, - OPNFVRole, - Image, - HostProfile, - GenericResourceBundle, - GenericResource, - GenericHost, - HostConfiguration -) - - -class ConfigUtil(): - count = 0 - - @staticmethod - def makeScenario(): - return Scenario.objects.create(name="testScenario") - - @staticmethod - def makeInstaller(): - inst = Installer.objects.create(name="testInstaller") - inst.sup_scenarios = [ConfigUtil.makeScenario()] - return inst - - @staticmethod - def makeOpsys(): - os = Opsys.objects.create(name="test Operating System") - os.sup_installers = [ConfigUtil.makeInstaller()] - return os - - @staticmethod - def makeConfigBundle(): - user = User.objects.create(username="test_user" + str(ConfigUtil.count)) - ConfigUtil.count += 1 - return ConfigBundle.objects.create(owner=user) - - @staticmethod - def makeOPNFVConfig(): - installer = ConfigUtil.makeInstaller() - scenario = ConfigUtil.makeScenario() - bundle = ConfigUtil.makeConfigBundle() - return OPNFVConfig.objects.create( - installer=installer, - scenario=scenario, - bundle=bundle - ) - - @staticmethod - def makeOPNFVRole(): - return OPNFVRole.objects.create( - name="Test role", - description="This is a test role" - ) - - @staticmethod - def makeImage(): - owner = User.objects.create(username="another test user") - lab_user = User.objects.create(username="labUserForTests") - lab = Lab.objects.create( - lab_user=lab_user, - name="this is lab for testing", - contact_email="email@mail.com", - contact_phone="123-4567" - ) - - return Image.objects.create( - cobbler_id="profile1", - from_lab=lab, - name="an image for testing", - owner=owner - ) - - @staticmethod - def makeGenericHost(): - profile = HostProfile.objects.create( - host_type=0, - name="test lab for config bundle", - description="this is a test profile" - ) - user = User.objects.create(username="test sample user 12") - bundle = GenericResourceBundle.objects.create( - name="Generic bundle for config tests", - xml="", - owner=user, - description="" - ) - - resource = GenericResource.objects.create( - bundle=bundle, - name="a test generic resource" - ) - - return GenericHost.objects.create( - profile=profile, - resource=resource - ) - - @staticmethod - def makeHostConfiguration(): - host = ConfigUtil.makeGenericHost() - image = ConfigUtil.makeImage() - bundle = ConfigUtil.makeConfigBundle() - opnfvRole = ConfigUtil.makeOPNFVRole() - return HostConfiguration.objects.create( - host=host, - image=image, - bundle=bundle, - opnfvRole=opnfvRole - ) - - -class ScenarioTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeScenario()) - - -class InstallerTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeInstaller()) - - -class OperatingSystemTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeOpsys()) - - -class ConfigBundleTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeConfigBundle()) - - -class OPNFVConfigTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeOPNFVConfig()) - - -class OPNFVRoleTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeOPNFVRole()) - - -class HostConfigurationTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeHostConfiguration()) - - -class ImageTestCase(TestCase): - - def test_save(self): - self.assertTrue(ConfigUtil.makeImage()) diff --git a/src/resource_inventory/urls.py b/src/resource_inventory/urls.py index a9a4d43..f9bd07e 100644 --- a/src/resource_inventory/urls.py +++ b/src/resource_inventory/urls.py @@ -7,30 +7,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - -""" -laas_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 resource_inventory.views import HostView, hostprofile_detail_view - - -app_name = 'resource' -urlpatterns = [ - url(r'^hosts$', HostView.as_view(), name='hosts'), - url(r'^profiles/(?P<hostprofile_id>.+)/$', hostprofile_detail_view, name='host_detail'), -] diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py index 52f8c75..f903394 100644 --- a/src/resource_inventory/views.py +++ b/src/resource_inventory/views.py @@ -6,33 +6,3 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - - -from django.views.generic import TemplateView -from django.shortcuts import get_object_or_404 -from django.shortcuts import render - -from resource_inventory.models import ResourceProfile, ResourceQuery - - -class HostView(TemplateView): - template_name = "resource/hosts.html" - - def get_context_data(self, **kwargs): - context = super(HostView, self).get_context_data(**kwargs) - hosts = ResourceQuery.filter(working=True) - context.update({'hosts': hosts, 'title': "Hardware Resources"}) - return context - - -def hostprofile_detail_view(request, hostprofile_id): - hostprofile = get_object_or_404(ResourceProfile, id=hostprofile_id) - - return render( - request, - "resource/hostprofile_detail.html", - { - 'title': "Host Type: " + str(hostprofile.name), - 'hostprofile': hostprofile - } - ) diff --git a/src/static/css/base.css b/src/static/css/base.css index 12364bd..b6ab104 100644 --- a/src/static/css/base.css +++ b/src/static/css/base.css @@ -69,17 +69,39 @@ a[aria-expanded="true"] > i.rotate { /* Booking Node Styles */ .selected_node { border-color: #40c640; + border-width: 2px; box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(109, 243, 76, 0.6); transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s; } +.invalid_field { + border-color: #c65040; + box-shadow: 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(243, 76, 76, 0.6); + transition: border-color ease-in-out .1s,box-shadow ease-in-out .1s; +} + /* Cursor effects */ .not-allowed { cursor: not-allowed; } +.z-n1 { + z-index: -1 !important; +} + +.z-0 { + z-index: 0 !important; +} + +.z-1 { +z-index: 1 !important; +} + .z-2 { - z-index: 2; + z-index: 2 !important; +} +.z-3 { + z-index: 3 !important; } .mh-30vh { @@ -91,3 +113,103 @@ a[aria-expanded="true"] > i.rotate { white-space: nowrap; overflow: hidden; } + +/* Design a pod Styles */ +.scroll-container { + position: absolute !important; /* Needed for proper functionality*/ + overflow: auto; + scroll-snap-type: y proximity; +} + +.scroll-area { + scroll-snap-align: start; + scroll-snap-stop: always; + min-height: 100vh; +} + +.add-button { + font-size: 3em; + font-weight: bolder; + text-align: center; + height: 2em; + width: 2em; + margin-left: auto; + margin-right: auto; +} + +.cancel-book-button { + font-size: 1em; + font-weight: bolder; + text-align: center; + height: 2em; + width: 10em; +} + +.input-search:focus { + border-color: none !important; + box-shadow: none !important; +} + +.arrow { + box-sizing: border-box; + height: 2vw; + width: 2vw; + border-style: solid; + border-color: black; + border-width: 0px 3px 3px 0px; + transition: border-width 150ms ease-in-out; + box-shadow: 0, 0, 100px, 100px, black; +} + +.arrow-down { + transform: rotate(45deg); +} + +.arrow-up { + transform: rotate(225deg); +} + +#next { + position: fixed !important; + bottom: 0; + left: 0; + background-color: white; + align-items: center; + justify-content: center; + z-index: 2 !important; +} + +#prev { + position: fixed !important; + left: 0; + background-color: white; + align-items: center; + justify-content: center; + z-index: 2 !important; +} + +#next:hover,#next:active { + background-color: #d4d4d4; +} + +#prev:hover,#prev:active { + background-color: #d4d4d4; +} + +.btn-workflow-nav { + box-shadow: none !important; +} + +.interface-btn { + color: inherit; +} + +.card-body-scroll { + height: 25vh; + overflow-y: auto; +} + +.overflow-control { + overflow-y: auto; + overflow-x: hidden; +}
\ No newline at end of file diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js deleted file mode 100644 index a63c71b..0000000 --- a/src/static/js/dashboard.js +++ /dev/null @@ -1,1664 +0,0 @@ -/////////////////// -// Global Variables -/////////////////// - -form_submission_callbacks = []; //all runnables will be executed before form submission - -/////////////////// -// Global Functions -/////////////////// - -// Taken from https://docs.djangoproject.com/en/3.0/ref/csrf/ -function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie !== '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i].trim(); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; -} - -function update_page(response) { - if( response.redirect ) - { - window.location.replace(response.redirect); - return; - } - draw_breadcrumbs(response.meta); - update_exit_button(response.meta); - update_side_buttons(response.meta); - $("#formContainer").html(response.content); -} - -function update_side_buttons(meta) { - const step = meta.active; - const page_count = meta.steps.length; - - const back_button = document.getElementById("workflow-nav-back"); - if (step == 0) { - back_button.classList.add("disabled"); - back_button.disabled = true; - } else { - back_button.classList.remove("disabled"); - back_button.disabled = false; - } - - const forward_btn = document.getElementById("workflow-nav-next"); - if (step == page_count - 1) { - forward_btn.classList.add("disabled"); - forward_btn.disabled = true; - } else { - forward_btn.classList.remove("disabled"); - forward_btn.disabled = false; - } -} - -function update_exit_button(meta) { - if (meta.workflow_count == 1) { - document.getElementById("cancel_btn").innerText = "Exit Workflow"; - } else { - document.getElementById("cancel_btn").innerText = "Return to Parent"; - } -} - -function draw_breadcrumbs(meta) { - $("#topPagination").children().not(".page-control").remove(); - - for (const i in meta.steps) { - const step_btn = create_step(meta.steps[i], i == meta["active"]); - $("#topPagination li:last-child").before(step_btn); - } -} - -function create_step(step_json, active) { - const step_dom = document.createElement("li"); - // First create the dom object depending on active or not - step_dom.className = "topcrumb"; - if (active) { - step_dom.classList.add("active"); - } - $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`) - - const code = step_json.valid; - - let stat = ""; - let msg = ""; - if (code < 100) { - $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>") - stat = ""; - msg = ""; - } else if (code < 200) { - $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>") - stat = "invalid"; - msg = step_json.message; - } else if (code < 300) { - $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>") - stat = "valid"; - msg = step_json.message; - } - - if (step_json.enabled == false) { - step_dom.classList.add("disabled"); - } - if (active) { - update_message(msg, stat); - } - - return step_dom; -} - -function update_description(title, desc) { - document.getElementById("view_title").innerText = title; - document.getElementById("view_desc").innerText = desc; -} - -function update_message(message, stepstatus) { - let color_code; - if (stepstatus == 'valid') { - color_code = 'text-success'; - } else if (stepstatus == 'invalid') { - color_code = 'text-danger'; - } else { - color_code = 'none'; - } - document.getElementById("view_message").innerText = message; - document.getElementById("view_message").className = "step_message"; - document.getElementById("view_message").classList.add("message_" + stepstatus); - document.getElementById("view_message").classList.add(color_code); -} - -function submitStepForm(next_step = "current"){ - run_form_callbacks(); - const step_form_data = $("#step_form").serialize(); - const form_data = $.param({ - "step": next_step, - "step_form": step_form_data, - "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val() - }); - $.post( - '/workflow/manager/', - form_data, - (data) => update_page(data), - 'json' - ).fail(() => alert("failure")); -} - -function run_form_callbacks(){ - for(f of form_submission_callbacks) - f(); - form_submission_callbacks = []; -} - -function create_workflow(type) { - $.ajax({ - type: "POST", - url: "/workflow/create/", - data: { - "workflow_type": type - }, - headers: { - "X-CSRFToken": getCookie('csrftoken') - } - }).done(function (data, textStatus, jqXHR) { - window.location = "/workflow/"; - }).fail(function (jqxHR, textstatus) { - alert("Something went wrong..."); - }); -} - -function add_workflow(type) { - data = $.ajax({ - type: "POST", - url: "/workflow/add/", - data: { - "workflow_type": type - }, - headers: { - "X-CSRFToken": getCookie('csrftoken') - } - }).done(function (data, textStatus, jqXHR) { - update_page(data); - }).fail(function (jqxHR, textstatus) { - alert("Something went wrong..."); - }); -} - -function pop_workflow() { - data = $.ajax({ - type: "POST", - url: "/workflow/pop/", - headers: { - "X-CSRFToken": getCookie('csrftoken') - } - }).done(function (data, textStatus, jqXHR) { - update_page(data); - }).fail(function (jqxHR, textstatus) { - alert("Something went wrong..."); - }); -} - -function continue_workflow() { - window.location.replace("/workflow/"); -} - -/////////////////// -//Class Definitions -/////////////////// - -class MultipleSelectFilterWidget { - - constructor(neighbors, items, initial) { - this.inputs = []; - this.graph_neighbors = neighbors; - this.filter_items = items; - this.currentLab = null; - this.available_resources = {}; - this.result = {}; - this.dropdown_count = 0; - - for(let nodeId in this.filter_items) { - const node = this.filter_items[nodeId]; - this.result[node.class] = {} - } - - this.make_selection(initial); - } - - make_selection(initial_data){ - if(!initial_data || jQuery.isEmptyObject(initial_data)) - return; - - // Need to sort through labs first - let initial_lab = initial_data['lab']; - let initial_resources = initial_data['resource']; - - for( let node_id in initial_lab) { // This should only be length one - const node = this.filter_items[node_id]; - const selection_data = initial_lab[node_id]; - if( selection_data.selected ) { - this.select(node); - this.markAndSweep(node); - this.updateResult(node); - } - if(node['multiple']){ - this.make_multiple_selection(node, selection_data); - } - this.currentLab = node; - this.available_resources = JSON.parse(node['available_resources']); - } - - for( let node_id in initial_resources){ - const node = this.filter_items[node_id]; - const selection_data = initial_resources[node_id]; - if( selection_data.selected ) { - this.select(node); - this.markAndSweep(node); - this.updateResult(node); - } - if(node['multiple']){ - this.make_multiple_selection(node, selection_data); - } - } - this.updateAvailibility(); - } - - make_multiple_selection(node, selection_data){ - const prepop_data = selection_data.values; - for(let k in prepop_data){ - const div = this.add_item_prepopulate(node, prepop_data[k]); - this.updateObjectResult(node, div.id, prepop_data[k]); - } - } - - markAndSweep(root){ - for(let i in this.filter_items) { - const node = this.filter_items[i]; - node['marked'] = true; //mark all nodes - } - - const toCheck = [root]; - while(toCheck.length > 0){ - const node = toCheck.pop(); - - if(!node['marked']) { - continue; //already visited, just continue - } - - node['marked'] = false; //mark as visited - if(node['follow'] || node == root){ //add neighbors if we want to follow this node - const neighbors = this.graph_neighbors[node.id]; - for(let neighId of neighbors) { - const neighbor = this.filter_items[neighId]; - toCheck.push(neighbor); - } - } - } - - //now remove all nodes still marked - for(let i in this.filter_items){ - const node = this.filter_items[i]; - if(node['marked']){ - this.disable_node(node); - } - } - } - - process(node) { - if(node['selected']) { - this.markAndSweep(node); - } - else { //TODO: make this not dumb - const selected = [] - //remember the currently selected, then reset everything and reselect one at a time - for(let nodeId in this.filter_items) { - node = this.filter_items[nodeId]; - if(node['selected']) { - selected.push(node); - } - this.clear(node); - } - for(let node of selected) { - this.select(node); - this.markAndSweep(node); - } - } - } - - select(node) { - const elem = document.getElementById(node['id']); - node['selected'] = true; - elem.classList.remove('bg-white', 'not-allowed', 'bg-light'); - elem.classList.add('selected_node'); - - if(node['class'] == 'resource') - this.reserveResource(node); - - } - - clear(node) { - const elem = document.getElementById(node['id']); - node['selected'] = false; - node['selectable'] = true; - elem.classList.add('bg-white') - elem.classList.remove('not-allowed', 'bg-light', 'selected_node'); - } - - disable_node(node) { - const elem = document.getElementById(node['id']); - node['selected'] = false; - node['selectable'] = false; - elem.classList.remove('bg-white', 'selected_node'); - elem.classList.add('not-allowed', 'bg-light'); - } - - labCheck(node){ - // if lab is not already selected update available resources - if(!node['selected']) { - this.currentLab = node; - this.available_resources = JSON.parse(node['available_resources']); - this.updateAvailibility(); - } else { - // a lab is already selected, clear already selected resources - if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) { - location.reload(); - return false; - } - } - return true; - } - - updateAvailibility() { - const lab_resources = this.graph_neighbors[this.currentLab.id]; - - // need to loop through and update all quantities - for(let i in lab_resources) { - const resource_node = this.filter_items[lab_resources[i]]; - const required_resources = JSON.parse(resource_node['required_resources']); - let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0]; - let leastAvailable = 100; - let currCount; - let quantityDescription; - let quantityNode; - - for(let resource in required_resources) { - currCount = Math.floor(this.available_resources[resource] / required_resources[resource]); - if(currCount < leastAvailable) - leastAvailable = currCount; - - if(!currCount || currCount < 0) { - leastAvailable = 0 - break; - } - } - - if (elem.children[0]){ - elem.removeChild(elem.children[0]); - } - - quantityDescription = '<br> Quantity Currently Available: ' + leastAvailable; - quantityNode = document.createElement('P'); - if (leastAvailable > 0) { - quantityDescription = quantityDescription.fontcolor('green'); - } else { - quantityDescription = quantityDescription.fontcolor('red'); - } - - quantityNode.innerHTML = quantityDescription; - elem.appendChild(quantityNode) - } - } - - reserveResource(node){ - const required_resources = JSON.parse(node['required_resources']); - let hostname = document.getElementById('id_hostname'); - let image = document.getElementById('id_image'); - let cnt = 0 - - - for(let resource in required_resources){ - this.available_resources[resource] -= required_resources[resource]; - cnt += required_resources[resource]; - } - - if (cnt > 1 && hostname) { - hostname.readOnly = true; - // we only disable hostname modification because there is no sane case where you want all hosts to have the same hostname - // image is still allowed to be set across all hosts, but is filtered to the set of images that are commonly applicable still - // if no images exist that would apply to all hosts in a pod, then the user is restricted to not setting an image - // and the default image for each host is used - } - - this.updateAvailibility(); - } - - releaseResource(node){ - const required_resources = JSON.parse(node['required_resources']); - let hostname = document.getElementById('id_hostname'); - let image = document.getElementById('id_image'); - - for(let resource in required_resources){ - this.available_resources[resource] += required_resources[resource]; - } - - if (hostname && image) { - hostname.readOnly = false; - image.disabled = false; - } - - this.updateAvailibility(); - } - - processClick(id){ - let lab_check; - const node = this.filter_items[id]; - if(!node['selectable']) - return; - - // If they are selecting a lab, update accordingly - if (node['class'] == 'lab') { - lab_check = this.labCheck(node); - if (!lab_check) - return; - } - - // Can only select a resource if a lab is selected - if (!this.currentLab) { - alert('You must select a lab before selecting a resource'); - return; - } - - if(node['multiple']){ - return this.processClickMultiple(node); - } else { - return this.processClickSingle(node); - } - } - - processClickSingle(node){ - node['selected'] = !node['selected']; //toggle on click - if(node['selected']) { - this.select(node); - } else { - this.clear(node); - this.releaseResource(node); // can't do this in clear since clear removes border - } - this.process(node); - this.updateResult(node); - } - - processClickMultiple(node){ - this.select(node); - const div = this.add_item_prepopulate(node, false); - this.process(node); - this.updateObjectResult(node, div.id, ""); - } - - restrictchars(input){ - if( input.validity.patternMismatch ){ - input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"); - input.reportValidity(); - } - input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, ""); - this.checkunique(input); - } - - checkunique(tocheck){ //TODO: use set - const val = tocheck.value; - for( let input of this.inputs ){ - if( input.value == val && input != tocheck){ - tocheck.setCustomValidity("All hostnames must be unique"); - tocheck.reportValidity(); - return; - } - } - tocheck.setCustomValidity(""); - } - - make_remove_button(div, node){ - const button = document.createElement("BUTTON"); - button.type = "button"; - button.appendChild(document.createTextNode("Remove")); - button.classList.add("btn", "btn-danger", "d-inline-block"); - const that = this; - button.onclick = function(){ that.remove_dropdown(div.id, node.id); } - return button; - } - - make_input(div, node, prepopulate){ - const input = document.createElement("INPUT"); - input.type = node.form.type; - input.name = node.id + node.form.name - input.classList.add("form-control", "w-auto", "d-inline-block"); - input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})"; - input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed" - input.placeholder = node.form.placeholder; - this.inputs.push(input); - const that = this; - input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); }; - input.oninput = function() { that.restrictchars(this); }; - if(prepopulate) - input.value = prepopulate; - return input; - } - - add_item_prepopulate(node, prepopulate){ - const div = document.createElement("DIV"); - div.id = "dropdown_" + this.dropdown_count; - div.classList.add("card", "flex-row", "d-flex", "mb-2"); - this.dropdown_count++; - const label = document.createElement("H5") - label.appendChild(document.createTextNode(node['name'])) - label.classList.add("p-1", "m-1", "flex-grow-1"); - div.appendChild(label); - let remove_btn = this.make_remove_button(div, node); - remove_btn.classList.add("p-1", "m-1"); - div.appendChild(remove_btn); - document.getElementById("dropdown_wrapper").appendChild(div); - return div; - } - - remove_dropdown(div_id, node_id){ - const div = document.getElementById(div_id); - const node = this.filter_items[node_id] - const parent = div.parentNode; - div.parentNode.removeChild(div); - this.result[node.class][node.id]['count']--; - this.releaseResource(node); // This can't be done on clear b/c clear removes border - - //checks if we have removed last item in class - if(this.result[node.class][node.id]['count'] == 0){ - delete this.result[node.class][node.id]; - this.clear(node); - } - } - - updateResult(node){ - if(!node['multiple']){ - this.result[node.class][node.id] = {selected: node.selected, id: node.model_id} - if(!node.selected) - delete this.result[node.class][node.id]; - } - } - - updateObjectResult(node, childKey, childValue){ - if(!this.result[node.class][node.id]) - this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0} - - this.result[node.class][node.id]['count']++; - } - - finish(){ - document.getElementById("filter_field").value = JSON.stringify(this.result); - } -} - -class NetworkStep { - // expects: - // - // debug: bool - // resources: { - // id: { - // id: int, - // value: { - // description: string, - // }, - // interfaces: [ - // id: int, - // name: str, - // description: str, - // connections: [ - // { - // network: int, [networks.id] - // tagged: bool - // } - // ], - // ], - // } - // } - // networks: { - // id: { - // id: int, - // name: str, - // public: bool, - // } - // } - // - constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){ - if(!this.check_support()) { - console.log("Aborting, browser is not supported"); - return; - } - - this.currentWindow = null; - this.netCount = 0; - this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC']; - this.hostCount = 0; - this.lastHostBottom = 100; - this.networks = new Set(); - this.has_public_net = false; - this.debug = debug; - this.editor = new mxEditor(); - this.graph = this.editor.graph; - - window.global_graph = this.graph; - window.network_rr_index = 5; - - this.editor.setGraphContainer(graphContainer); - this.doGlobalConfig(); - - let mx_networks = {} - - for(const network_id in networks) { - let network = networks[network_id]; - - mx_networks[network_id] = this.populateNetwork(network); - } - - this.prefillHosts(resources, mx_networks); - - //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true); - //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true); - this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus'); - this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus'); - - if(this.debug){ - this.editor.addAction('printXML', function(editor, cell) { - mxLog.write(this.encodeGraph()); - mxLog.show(); - }.bind(this)); - this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code'); - } - - new mxOutline(this.graph, overviewContainer); - //sets the edge color to be the same as the network - this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this)); - //hooks up double click functionality - this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this); - } - - check_support(){ - if (!mxClient.isBrowserSupported()) { - mxUtils.error('Browser is not supported', 200, false); - return false; - } - return true; - } - - /** - * Expects - * mx_interface: mxCell for the interface itself - * network: mxCell for the outer network - * tagged: bool - */ - connectNetwork(mx_interface, network, tagged) { - var cell = new mxCell( - "connection from " + network + " to " + mx_interface, - new mxGeometry(0, 0, 50, 50)); - cell.edge = true; - cell.geometry.relative = true; - cell.setValue(JSON.stringify({tagged: tagged})); - - let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network); - let edge = this.graph.addEdge(cell, null, mx_interface, terminal); - this.colorEdge(edge, terminal, true); - this.graph.refresh(edge); - } - - /** - * Expects: - * - * to: desired y axis position of the matching cell - * within: graph cell for a full network, with all child cells - * - * Returns: - * an mx cell, the one vertically closest to the desired value - * - * Side effect: - * modifies the <rr_index> on the <within> parameter - */ - getClosestNetworkCell(to, within) { - if(window.network_rr_index === undefined) { - window.network_rr_index = 5; - } - - let child_keys = within.children.keys(); - let children = Array.from(within.children); - let index = (window.network_rr_index++) % children.length; - - let child = within.children[child_keys[index]]; - - return children[index]; - } - - /** Expects - * - * hosts: { - * id: { - * id: int, - * value: { - * description: string, - * }, - * interfaces: [ - * id: int, - * name: str, - * description: str, - * connections: [ - * { - * network: int, [networks.id] - * tagged: bool - * } - * ], - * ], - * } - * } - * - * network_mappings: { - * <django network id>: <mxnetwork id> - * } - * - * draws given hosts into the mxgraph - */ - prefillHosts(hosts, network_mappings){ - for(const host_id in hosts) { - this.makeHost(hosts[host_id], network_mappings); - } - } - - cellConnectionHandler(sender, event){ - const edge = event.getProperty('edge'); - const terminal = event.getProperty('terminal') - const source = event.getProperty('source'); - if(this.checkAllowed(edge, terminal, source)) { - this.colorEdge(edge, terminal, source); - this.alertVlan(edge, terminal, source); - } - } - - doubleClickHandler(evt, cell) { - if( cell != null ){ - if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) { - cell = cell.getParent(); - } - if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) { - this.createDeleteDialog(cell.getId()); - } - else { - this.showDetailWindow(cell); - } - } - } - - alertVlan(edge, terminal, source) { - if( terminal == null || edge.getTerminal(!source) == null) { - return; - } - const form = document.createElement("form"); - const tagged = document.createElement("input"); - tagged.type = "radio"; - tagged.name = "tagged"; - tagged.value = "True"; - tagged.checked = "True"; - form.appendChild(tagged); - form.appendChild(document.createTextNode(" Tagged")); - form.appendChild(document.createElement("br")); - - const untagged = document.createElement("input"); - untagged.type = "radio"; - untagged.name = "tagged"; - untagged.value = "False"; - form.appendChild(untagged); - form.appendChild(document.createTextNode(" Untagged")); - form.appendChild(document.createElement("br")); - - const yes_button = document.createElement("button"); - yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this); - yes_button.appendChild(document.createTextNode("Okay")); - - const cancel_button = document.createElement("button"); - cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this); - cancel_button.appendChild(document.createTextNode("Cancel")); - - const error_div = document.createElement("div"); - error_div.id = "current_window_errors"; - form.appendChild(error_div); - - const content = document.createElement('div'); - content.appendChild(form); - content.appendChild(yes_button); - content.appendChild(cancel_button); - this.showWindow("Vlan Selection", content, 200, 200); - } - - createDeleteDialog(id) { - const content = document.createElement('div'); - const remove_button = document.createElement("button"); - remove_button.style.width = '46%'; - remove_button.onclick = function() { this.deleteCell(id);}.bind(this); - remove_button.appendChild(document.createTextNode("Remove")); - const cancel_button = document.createElement("button"); - cancel_button.style.width = '46%'; - cancel_button.onclick = function() { this.closeWindow();}.bind(this); - cancel_button.appendChild(document.createTextNode("Cancel")); - - content.appendChild(remove_button); - content.appendChild(cancel_button); - this.showWindow('Do you want to delete this network?', content, 200, 62); - } - - checkAllowed(edge, terminal, source) { - //check if other terminal is null, and that they are different - const otherTerminal = edge.getTerminal(!source); - if(terminal != null && otherTerminal != null) { - if( terminal.getParent().getId().split('_')[0] == //'host' or 'network' - otherTerminal.getParent().getId().split('_')[0] ) { - //not allowed - this.graph.removeCells([edge]); - return false; - } - } - return true; - } - - colorEdge(edge, terminal, source) { - if(terminal.getParent().getId().indexOf('network') >= 0) { - const styles = terminal.getParent().getStyle().split(';'); - let color = 'black'; - for(let style of styles){ - const kvp = style.split('='); - if(kvp[0] == "fillColor"){ - color = kvp[1]; - } - } - - edge.setStyle('strokeColor=' + color); - } else { - console.log("Failed to color " + edge + ", " + terminal + ", " + source); - } - } - - showDetailWindow(cell) { - const info = JSON.parse(cell.getValue()); - const content = document.createElement("div"); - const pre_tag = document.createElement("pre"); - pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description)); - const ok_button = document.createElement("button"); - ok_button.onclick = function() { this.closeWindow();}; - content.appendChild(pre_tag); - content.appendChild(ok_button); - this.showWindow('Details', content, 400, 400); - } - - restoreFromXml(xml, editor) { - const doc = mxUtils.parseXml(xml); - const node = doc.documentElement; - editor.readGraphModel(node); - - //Iterate over all children, and parse the networks to add them to the sidebar - for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) { - if(cell.getId().indexOf("network") > -1) { - const info = JSON.parse(cell.getValue()); - const name = info['name']; - this.networks.add(name); - const styles = cell.getStyle().split(";"); - let color = null; - for(const style of styles){ - const kvp = style.split('='); - if(kvp[0] == "fillColor") { - color = kvp[1]; - break; - } - } - if(info.public){ - this.has_public_net = true; - } - this.netCount++; - this.makeSidebarNetwork(name, color, cell.getId()); - } - } - } - - deleteCell(cellId) { - var cell = this.graph.getModel().getCell(cellId); - if( cellId.indexOf("network") > -1 ) { - let elem = document.getElementById(cellId); - elem.parentElement.removeChild(elem); - } - this.graph.removeCells([cell]); - this.currentWindow.destroy(); - } - - newNetworkWindow() { - const input = document.createElement("input"); - input.type = "text"; - input.name = "net_name"; - input.maxlength = 100; - input.id = "net_name_input"; - input.style.margin = "5px"; - - const yes_button = document.createElement("button"); - yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this); - yes_button.appendChild(document.createTextNode("Okay")); - - const cancel_button = document.createElement("button"); - cancel_button.onclick = function() {this.closeWindow();}.bind(this); - cancel_button.appendChild(document.createTextNode("Cancel")); - - const error_div = document.createElement("div"); - error_div.id = "current_window_errors"; - - const content = document.createElement("div"); - content.appendChild(document.createTextNode("Name: ")); - content.appendChild(input); - content.appendChild(document.createElement("br")); - content.appendChild(yes_button); - content.appendChild(cancel_button); - content.appendChild(document.createElement("br")); - content.appendChild(error_div); - - this.showWindow("Network Creation", content, 300, 300); - } - - parseNetworkWindow() { - const net_name = document.getElementById("net_name_input").value - const error_div = document.getElementById("current_window_errors"); - if( this.networks.has(net_name) ){ - error_div.innerHTML = "All network names must be unique"; - return; - } - this.addNetwork(net_name); - this.currentWindow.destroy(); - } - - addToolbarButton(editor, toolbar, action, image) { - const button = document.createElement('button'); - button.setAttribute('class', 'btn btn-sm m-1'); - if (image != null) { - const icon = document.createElement('i'); - icon.setAttribute('class', 'fas ' + image); - button.appendChild(icon); - } - mxEvent.addListener(button, 'click', function(evt) { - editor.execute(action); - }); - mxUtils.write(button, ''); - toolbar.appendChild(button); - }; - - encodeGraph() { - const encoder = new mxCodec(); - const xml = encoder.encode(this.graph.getModel()); - return mxUtils.getXml(xml); - } - - doGlobalConfig() { - //general graph stuff - this.graph.setMultigraph(false); - this.graph.setCellsSelectable(false); - this.graph.setCellsMovable(false); - - //testing - this.graph.vertexLabelIsMovable = true; - - //edge behavior - this.graph.setConnectable(true); - this.graph.setAllowDanglingEdges(false); - mxEdgeHandler.prototype.snapToTerminals = true; - mxConstants.MIN_HOTSPOT_SIZE = 16; - mxConstants.DEFAULT_HOTSPOT = 1; - //edge 'style' (still affects behavior greatly) - const style = this.graph.getStylesheet().getDefaultEdgeStyle(); - style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW; - style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE; - style[mxConstants.STYLE_ROUNDED] = true; - style[mxConstants.STYLE_FONTCOLOR] = 'black'; - style[mxConstants.STYLE_STROKECOLOR] = 'red'; - style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF'; - style[mxConstants.STYLE_STROKEWIDTH] = '3'; - style[mxConstants.STYLE_ROUNDED] = true; - style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; - - const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle(); - hostStyle[mxConstants.STYLE_ROUNDED] = 1; - - this.graph.convertValueToString = function(cell) { - try{ - //changes value for edges with xml value - if(cell.isEdge()) { - if(JSON.parse(cell.getValue())["tagged"]) { - return "tagged"; - } - return "untagged"; - } - else{ - return JSON.parse(cell.getValue())['name']; - } - } - catch(e){ - return cell.getValue(); - } - }; - } - - showWindow(title, content, width, height) { - //create transparent black background - const background = document.createElement('div'); - background.style.position = 'absolute'; - background.style.left = '0px'; - background.style.top = '0px'; - background.style.right = '0px'; - background.style.bottom = '0px'; - background.style.background = 'black'; - mxUtils.setOpacity(background, 50); - document.body.appendChild(background); - - const x = Math.max(0, document.body.scrollWidth/2-width/2); - const y = Math.max(10, (document.body.scrollHeight || - document.documentElement.scrollHeight)/2-height*2/3); - - const wnd = new mxWindow(title, content, x, y, width, height, false, true); - wnd.setClosable(false); - - wnd.addListener(mxEvent.DESTROY, function(evt) { - this.graph.setEnabled(true); - mxEffects.fadeOut(background, 50, true, 10, 30, true); - }.bind(this)); - this.currentWindow = wnd; - - this.graph.setEnabled(false); - this.currentWindow.setVisible(true); - }; - - closeWindow() { - //allows the current window to be destroyed - this.currentWindow.destroy(); - }; - - othersUntagged(edgeID) { - const edge = this.graph.getModel().getCell(edgeID); - const end1 = edge.getTerminal(true); - const end2 = edge.getTerminal(false); - - if( end1.getParent().getId().split('_')[0] == 'host' ){ - var netint = end1; - } else { - var netint = end2; - } - - var edges = netint.edges; - for( let edge of edges) { - if( edge.getValue() ) { - var tagged = JSON.parse(edge.getValue()).tagged; - } else { - var tagged = true; - } - if( !tagged ) { - return true; - } - } - - return false; - }; - - - deleteVlanWindow(edgeID) { - const cell = this.graph.getModel().getCell(edgeID); - this.graph.removeCells([cell]); - this.currentWindow.destroy(); - } - - parseVlanWindow(edgeID) { - //do parsing and data manipulation - const radios = document.getElementsByName("tagged"); - const edge = this.graph.getModel().getCell(edgeID); - - for(let radio of radios){ - if(radio.checked) { - //set edge to be tagged or untagged - if( radio.value == "False") { - if( this.othersUntagged(edgeID) ) { - document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed."; - return; - } - } - const edgeVal = {tagged: radio.value == "True"}; - edge.setValue(JSON.stringify(edgeVal)); - break; - } - } - this.graph.refresh(edge); - this.closeWindow(); - } - - makeMxNetwork(net_name, is_public = false) { - const model = this.graph.getModel(); - const width = 10; - const height = 1700; - const xoff = 400 + (30 * this.netCount); - const yoff = -10; - let color = this.netColors[this.netCount]; - if( this.netCount > (this.netColors.length - 1)) { - color = Math.floor(Math.random() * 16777215); //int in possible color space - color = '#' + color.toString(16).toUpperCase(); //convert to hex - } - const net_val = { name: net_name, public: is_public}; - const net = this.graph.insertVertex( - this.graph.getDefaultParent(), - 'network_' + this.netCount, - JSON.stringify(net_val), - xoff, - yoff, - width, - height, - 'fillColor=' + color, - false - ); - const num_ports = 45; - for(var i=0; i<num_ports; i++){ - let port = this.graph.insertVertex( - net, - null, - '', - 0, - (1/num_ports) * i, - 10, - height / num_ports, - 'fillColor=black;opacity=0', - true - ); - } - - const ret_val = { color: color, element_id: "network_" + this.netCount }; - - this.networks.add(net_name); - this.netCount++; - return ret_val; - } - - // expects: - // - // { - // id: int, - // name: str, - // public: bool, - // } - // - // returns: - // mxgraph id of network - populateNetwork(network) { - let mxNet = this.makeMxNetwork(network.name, network.public); - this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id); - - if( network.public ) { - this.has_public_net = true; - } - - return mxNet.element_id; - } - - addPublicNetwork() { - const net = this.makeMxNetwork("public", true); - this.makeSidebarNetwork("public", net['color'], net['element_id']); - this.has_public_net = true; - } - - addNetwork(net_name) { - const ret = this.makeMxNetwork(net_name); - this.makeSidebarNetwork(net_name, ret.color, ret.element_id); - } - - updateHosts(removed) { - const cells = [] - for(const hostID of removed) { - cells.push(this.graph.getModel().getCell("host_" + hostID)); - } - this.graph.removeCells(cells); - - const hosts = this.graph.getChildVertices(this.graph.getDefaultParent()); - let topdist = 100; - for(const i in hosts) { - const host = hosts[i]; - if(host.id.startsWith("host_")){ - const geometry = host.getGeometry(); - geometry.y = topdist + 50; - topdist = geometry.y + geometry.height; - host.setGeometry(geometry); - } - } - } - - makeSidebarNetwork(net_name, color, net_id){ - const colorBlob = document.createElement("div"); - colorBlob.className = "square-20 rounded-circle"; - colorBlob.style['background'] = color; - - const textContainer = document.createElement("span"); - textContainer.className = "ml-2"; - textContainer.appendChild(document.createTextNode(net_name)); - - const timesIcon = document.createElement("i"); - timesIcon.classList.add("fas", "fa-times"); - - const deletebutton = document.createElement("button"); - deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center"; - deletebutton.appendChild(timesIcon); - deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false); - - const newNet = document.createElement("li"); - newNet.classList.add("list-group-item", "d-flex", "bg-light"); - newNet.id = net_id; - newNet.appendChild(colorBlob); - newNet.appendChild(textContainer); - - if( net_name != "public" ) { - newNet.appendChild(deletebutton); - } - document.getElementById("network_list").appendChild(newNet); - } - - /** - * Expects format: - * { - * 'id': int, - * 'value': { - * 'description': string, - * }, - * 'interfaces': [ - * { - * id: int, - * name: str, - * description: str, - * connections: [ - * { - * network: int, <django network id>, - * tagged: bool - * } - * ] - * } - * ] - * } - * - * network_mappings: { - * <django network id>: <mxnetwork id> - * } - */ - makeHost(hostInfo, network_mappings) { - const value = JSON.stringify(hostInfo['value']); - const interfaces = hostInfo['interfaces']; - const width = 100; - const height = (25 * interfaces.length) + 25; - const xoff = 75; - const yoff = this.lastHostBottom + 50; - this.lastHostBottom = yoff + height; - const host = this.graph.insertVertex( - this.graph.getDefaultParent(), - 'host_' + hostInfo['id'], - value, - xoff, - yoff, - width, - height, - 'editable=0', - false - ); - host.getGeometry().offset = new mxPoint(-50,0); - host.setConnectable(false); - this.hostCount++; - - for(var i=0; i<interfaces.length; i++) { - const port = this.graph.insertVertex( - host, - null, - JSON.stringify(interfaces[i]), - 90, - (i * 25) + 12, - 20, - 20, - 'fillColor=blue;editable=0', - false - ); - port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0); - const iface = interfaces[i]; - for( const connection of iface.connections ) { - const network = this - .graph - .getModel() - .getCell(network_mappings[connection.network]); - - this.connectNetwork(port, network, connection.tagged); - } - this.graph.refresh(port); - } - this.graph.refresh(host); - } - - prepareForm() { - const input_elem = document.getElementById("hidden_xml_input"); - input_elem.value = this.encodeGraph(this.graph); - } -} - -class SearchableSelectMultipleWidget { - constructor(format_vars, field_dataset, field_initial) { - this.format_vars = format_vars; - this.items = field_dataset; - this.initial = field_initial; - - this.expanded_name_trie = {"isComplete": false}; - this.small_name_trie = {"isComplete": false}; - this.string_trie = {"isComplete": false}; - - this.added_items = new Set(); - - for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] ) - { - this[e] = format_vars[e]; - } - - this.search_field_init(); - - if( this.show_from_noentry ) - { - this.search(""); - } - } - - disable() { - const textfield = document.getElementById("user_field"); - const drop = document.getElementById("drop_results"); - - textfield.disabled = "True"; - drop.style.display = "none"; - - const btns = document.getElementsByClassName("btn-remove"); - for( const btn of btns ) - { - btn.classList.add("disabled"); - btn.onclick = ""; - } - } - - search_field_init() { - this.build_all_tries(this.items); - - for( const elem of this.initial ) - { - this.select_item(elem); - } - if(this.initial.length == 1) - { - this.search(this.items[this.initial[0]]["small_name"]); - document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"]; - } - } - - build_all_tries(dict) - { - for( const key in dict ) - { - this.add_item(dict[key]); - } - } - - add_item(item) - { - const id = item['id']; - this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie); - this.add_to_tree(item['small_name'], id, this.small_name_trie); - this.add_to_tree(item['string'], id, this.string_trie); - } - - add_to_tree(str, id, trie) - { - let inner_trie = trie; - while( str ) - { - if( !inner_trie[str.charAt(0)] ) - { - var new_trie = {}; - inner_trie[str.charAt(0)] = new_trie; - } - else - { - var new_trie = inner_trie[str.charAt(0)]; - } - - if( str.length == 1 ) - { - new_trie.isComplete = true; - if( !new_trie.ids ) - { - new_trie.ids = []; - } - new_trie.ids.push(id); - } - inner_trie = new_trie; - str = str.substring(1); - } - } - - search(input) - { - if( input.length == 0 && !this.show_from_noentry){ - this.dropdown([]); - return; - } - else if( input.length == 0 && this.show_from_noentry) - { - this.dropdown(this.items); //show all items - } - else - { - const trees = [] - const tr1 = this.getSubtree(input, this.expanded_name_trie); - trees.push(tr1); - const tr2 = this.getSubtree(input, this.small_name_trie); - trees.push(tr2); - const tr3 = this.getSubtree(input, this.string_trie); - trees.push(tr3); - const results = this.collate(trees); - this.dropdown(results); - } - } - - getSubtree(input, given_trie) - { - /* - recursive function to return the trie accessed at input - */ - - if( input.length == 0 ){ - return given_trie; - } - - else{ - const substr = input.substring(0, input.length - 1); - const last_char = input.charAt(input.length-1); - const subtrie = this.getSubtree(substr, given_trie); - - if( !subtrie ) //substr not in the trie - { - return {}; - } - - const indexed_trie = subtrie[last_char]; - return indexed_trie; - } - } - - serialize(trie) - { - /* - takes in a trie and returns a list of its item id's - */ - let itemIDs = []; - if ( !trie ) - { - return itemIDs; //empty, base case - } - for( const key in trie ) - { - if(key.length > 1) - { - continue; - } - itemIDs = itemIDs.concat(this.serialize(trie[key])); - } - if ( trie.isComplete ) - { - itemIDs.push(...trie.ids); - } - - return itemIDs; - } - - collate(trees) - { - /* - takes a list of tries - returns a list of ids of objects that are available - */ - const results = []; - for( const tree of trees ) - { - const available_IDs = this.serialize(tree); - - for( const itemID of available_IDs ) { - results[itemID] = this.items[itemID]; - } - } - return results; - } - - generate_element_text(obj) - { - const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x)); - const result = content_strings.shift(); - if( result == null || content_strings.length < 1) { - return result; - } else { - return result + " (" + content_strings.join(", ") + ")"; - } - } - - dropdown(ids) - { - /* - takes in a mapping of ids to objects in items - and displays them in the dropdown - */ - const drop = document.getElementById("drop_results"); - while(drop.firstChild) - { - drop.removeChild(drop.firstChild); - } - - for( const id in ids ) - { - const obj = this.items[id]; - const result_text = this.generate_element_text(obj); - const result_entry = document.createElement("a"); - result_entry.href = "#"; - result_entry.innerText = result_text; - result_entry.title = result_text; - result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0"); - result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); }; - const tooltip = document.createElement("span"); - const tooltiptext = document.createTextNode(result_text); - tooltip.appendChild(tooltiptext); - tooltip.classList.add("d-none"); - result_entry.appendChild(tooltip); - drop.appendChild(result_entry); - } - - const scroll_restrictor = document.getElementById("scroll_restrictor"); - - if( !drop.firstChild ) - { - scroll_restrictor.style.visibility = 'hidden'; - } - else - { - scroll_restrictor.style.visibility = 'inherit'; - } - } - - select_item(item_id) - { - if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 ) - { - this.added_items.add(item_id); - } - this.update_selected_list(); - // clear search bar contents - document.getElementById("user_field").value = ""; - document.getElementById("user_field").focus(); - this.search(""); - } - - remove_item(item_id) - { - this.added_items.delete(item_id); - - this.update_selected_list() - document.getElementById("user_field").focus(); - } - - update_selected_list() - { - document.getElementById("added_number").innerText = this.added_items.size; - const selector = document.getElementById('selector'); - selector.value = JSON.stringify([...this.added_items]); - const added_list = document.getElementById('added_list'); - - while(selector.firstChild) - { - selector.removeChild(selector.firstChild); - } - while(added_list.firstChild) - { - added_list.removeChild(added_list.firstChild); - } - - const list_html = document.createElement("div"); - list_html.classList.add("list-group"); - - for( const item_id of this.added_items ) - { - const times = document.createElement("li"); - times.classList.add("fas", "fa-times"); - - const deleteButton = document.createElement("a"); - deleteButton.href = "#"; - deleteButton.innerHTML = "<i class='fas fa-times'></i>" - // Setting .onclick/.addEventListener does not work, - // which is why I took the setAttribute approach - // If anyone knows why, please let me know :] - deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`); - deleteButton.classList.add("btn"); - const deleteColumn = document.createElement("div"); - deleteColumn.classList.add("col-auto"); - deleteColumn.append(deleteButton); - - const item = this.items[item_id]; - const element_entry_text = this.generate_element_text(item); - const textColumn = document.createElement("div"); - textColumn.classList.add("col", "overflow-ellipsis"); - textColumn.innerText = element_entry_text; - textColumn.title = element_entry_text; - - const itemRow = document.createElement("div"); - itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center"); - itemRow.append(textColumn, deleteColumn); - - list_html.append(itemRow); - } - added_list.innerHTML = list_html.innerHTML; - } -} diff --git a/src/static/js/workflows/book-a-pod.js b/src/static/js/workflows/book-a-pod.js new file mode 100644 index 0000000..d573342 --- /dev/null +++ b/src/static/js/workflows/book-a-pod.js @@ -0,0 +1,708 @@ +/** + * book-a-pod.js + */ + +const steps = { + SELECT_TEMPLATE: 0, + CLOUD_INIT: 1, + BOOKING_DETAILS: 2, + ADD_COLLABS: 3, + BOOKING_SUMMARY: 4 + } + + class BookingWorkflow extends Workflow { + constructor(savedBookingBlob) { + super(["select_template", "cloud_init", "booking_details" ,"add_collabs", "booking_summary"]) + + // if (savedBookingBlob) { + // this.resume_workflow() + // } + + this.bookingBlob = new BookingBlob({}); + this.userTemplates = null; + } + + async startWorkflow() { + this.userTemplates = await LibLaaSAPI.getTemplatesForUser() // List<TemplateBlob> + GUI.displayTemplates(this.userTemplates); + GUI.modifyCollabWidget(); + this.setEventListeners(); + document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'}); + } + + setEventListeners() { + const ci_textarea = document.getElementById('ci-textarea'); + ci_textarea.value = "" + ci_textarea.addEventListener('focusin', this.onFocusInCIFile); + ci_textarea.addEventListener('focusout', this.onFocusOutCIFile); + + const input_purpose = document.getElementById('input_purpose'); + input_purpose.value = "" + input_purpose.addEventListener('focusin', this.onFocusInPurpose); + input_purpose.addEventListener('focusout', this.onFocusOutPurpose) + + const input_project = document.getElementById('input_project'); + input_project.value = "" + input_project.addEventListener('focusin', this.onFocusInProject); + input_project.addEventListener('focusout', this.onFocusOutProject); + + const input_length = document.getElementById('input_length'); + input_length.value = 1; + } + + getTemplateBlobFromId(templateId) { + for (const t of this.userTemplates) { + if (t.id == templateId) return t + } + + return null + } + + onclickSelectTemplate(templateCard, templateId) { + this.step = steps.SELECT_TEMPLATE + const oldHighlight = document.querySelector("#default_templates_list .selected_node") + if (oldHighlight) { + GUI.unhighlightCard(oldHighlight) + } + + GUI.highlightCard(templateCard); + this.bookingBlob.template_id = templateId; + GUI.refreshSummaryHosts(this.getTemplateBlobFromId(templateId)); + } + + isValidCIFile(ci_file) { + // todo + return true; + } + + isValidProject(project) { + let passed = true + let message = "success" + + if (project == "") { + passed = false; + message = "Project field cannot be empty." + return[passed, message] + } + + if (!(project.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -]+$/i))) { + passed = false; + message = "Project field contains invalid characters" + return[passed, message] + } + + return [passed, message] + } + + isValidPurpose(purpose) { + let passed = true + let message = "success" + + if (purpose == "") { + passed = false; + message = "Purpose field cannot be empty." + return[passed, message] + } + + if (!(purpose.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -]+$/i))) { + passed = false; + message = "Purpose field contains invalid characters" + return[passed, message] + } + + return [passed, message] + } + + // Ci FIle + onFocusOutCIFile() { + const ci_textarea = document.getElementById('ci-textarea'); + if (workflow.isValidCIFile(ci_textarea.value)) { + workflow.bookingBlob.global_cifile = ci_textarea.value; + } else { + GUI.highlightError(ci_textarea); + } + } + + onFocusInCIFile() { + this.step = steps.CLOUD_INIT + const ci_textarea = document.getElementById('ci-textarea') + GUI.unhighlightError(ci_textarea) + } + + // Purpose + onFocusOutPurpose() { + const input = document.getElementById('input_purpose'); + const valid = workflow.isValidPurpose(input.value); + if (valid[0]) { + workflow.bookingBlob.metadata.purpose = input.value; + GUI.refreshSummaryDetails(workflow.bookingBlob.metadata) + } else { + GUI.showDetailsError(valid[1]) + GUI.highlightError(input); + } + } + + onFocusInPurpose() { + this.step = steps.BOOKING_DETAILS + const input = document.getElementById('input_purpose'); + GUI.hideDetailsError() + GUI.unhighlightError(input) + } + + // Project + onFocusOutProject() { + const input = document.getElementById('input_project'); + const valid = workflow.isValidProject(input.value); + if (valid[0]) { + workflow.bookingBlob.metadata.project = input.value; + GUI.refreshSummaryDetails(workflow.bookingBlob.metadata) + } else { + GUI.showDetailsError(valid[1]) + GUI.highlightError(input); + } + } + + onFocusInProject() { + this.step = steps.BOOKING_DETAILS + const input = document.getElementById('input_project'); + GUI.hideDetailsError() + GUI.unhighlightError(input) + } + + onchangeDays() { + this.step = steps.BOOKING_DETAILS + const counter = document.getElementById("booking_details_day_counter") + const input = document.getElementById('input_length') + workflow.bookingBlob.metadata.length = input.value + GUI.refreshSummaryDetails(workflow.bookingBlob.metadata) + counter.innerText = "Days: " + input.value + } + + add_collaborator(username) { + this.step = steps.ADD_COLLABS; + + for (const c of this.bookingBlob.allowed_users) { + if (c == username) { + return; + } + } + + this.bookingBlob.allowed_users.push(username) + GUI.refreshSummaryCollabs(this.bookingBlob.allowed_users) + } + + remove_collaborator(username) { + // Removes collab from collaborators list and updates summary + this.step = steps.ADD_COLLABS + + const temp = []; + + for (const c of this.bookingBlob.allowed_users) { + if (c != username) { + temp.push(c); + } + } + + this.bookingBlob.allowed_users = temp; + GUI.refreshSummaryCollabs(this.bookingBlob.allowed_users) + } + + isCompleteBookingInfo() { + let passed = true + let message = "success" + let section = steps.BOOKING_SUMMARY + + const blob = this.bookingBlob; + const meta = blob.metadata; + + if (blob.template_id == null) { + passed = false; + message = "Please select a template." + section = steps.SELECT_TEMPLATE + return [passed, message, section] + } + + if (meta.purpose == null || meta.project == null || meta.length == 0) { + passed = false + message = "Please finish adding booking details." + section = steps.BOOKING_DETAILS + return [passed, message, section] + } + + return[passed, message, section]; + } + + onclickCancel() { + if (confirm("Are you sure you wish to discard this booking?")) { + location.reload(); + } + } + + /** Async / await is more infectious than I thought, so all functions that rely on an API call will need to be async */ + async onclickConfirm() { + const complete = this.isCompleteBookingInfo(); + if (!complete[0]) { + alert(complete[1]); + this.step = complete[2] + document.getElementById(this.sections[complete[2]]).scrollIntoView({behavior: 'smooth'}); + return + } + if (confirm("Are you sure you would like to create this booking?")) { + const response = await LibLaaSAPI.makeBooking(this.bookingBlob); + if (response.bookingId) { + alert("The booking has been successfully created.") + window.location.href = "../../"; + } else { + alert("The booking could not be created at this time.") + } + } + } + + + + } + + +/** View class that displays cards and generates HTML + * Functions as a namespace, does not hold state +*/ +class GUI { + + static highlightCard(card) { + card.classList.add('selected_node'); + } + + static unhighlightCard(card) { + card.classList.remove('selected_node'); + } + + static highlightError(element) { + element.classList.add('invalid_field'); + } + + static unhighlightError(element) { + element.classList.remove("invalid_field"); + } + + /** Takes a list of templateBlobs and creates a selectable card for each of them */ + static displayTemplates(templates) { + const templates_list = document.getElementById("default_templates_list"); + + for (const t of templates) { + const newCard = this.makeTemplateCard(t); + templates_list.appendChild(newCard); + } + } + + static makeTemplateCard(templateBlob) { + const col = document.createElement('div'); + col.classList.add('col-3', 'my-1'); + col.innerHTML= ` + <div class="card"> + <div class="card-header"> + <p class="h5 font-weight-bold mt-2">` + templateBlob.pod_name + `</p> + </div> + <div class="card-body"> + <p class="grid-item-description">` + templateBlob.pod_desc +`</p> + </div> + <div class="card-footer"> + <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link" + onclick="workflow.onclickSelectTemplate(this.parentNode.parentNode, '` + templateBlob.id +`')">Select</button> + </div> + </div> + ` + return col; + } + + /** Removes default styling applied by django */ + static modifyCollabWidget() { + document.getElementsByTagName('label')[0].setAttribute('hidden', ''); + document.getElementById('addable_limit').setAttribute('hidden', ''); + document.getElementById('added_number').setAttribute('hidden', ''); + const user_field = document.getElementById('user_field'); + user_field.classList.add('border-top-0'); + document.querySelector('.form-group').classList.add('mb-0'); + + const added_list = document.getElementById('added_list'); + added_list.remove(); + document.getElementById('search_select_outer').appendChild(added_list); + } + + static showDetailsError(message) { + document.getElementById("booking_details_error").innerText = message; + } + + static hideDetailsError() { + document.getElementById("booking_details_error").innerText = ''; + } + + static refreshSummaryDetails(bookingMetaData) { + const ul = document.getElementById("booking_summary_booking_details") + ul.innerHTML = ''; + + if (bookingMetaData.project) { + const project_li = document.createElement('li'); + project_li.innerText = 'Project: ' + bookingMetaData.project; + ul.appendChild(project_li); + } + + if (bookingMetaData.purpose) { + const project_li = document.createElement('li'); + project_li.innerText = 'Purpose: ' + bookingMetaData.purpose; + ul.appendChild(project_li); + } + + if (bookingMetaData.length) { + const project_li = document.createElement('li'); + project_li.innerText = 'Length: ' + bookingMetaData.length + ' days'; + ul.appendChild(project_li); + } + } + + static refreshSummaryCollabs(collaborators) { + const collabs_ul = document.getElementById('booking_summary_collaborators'); + collabs_ul.innerHTML = ''; + for (const u of collaborators) { + const collabs_li = document.createElement('li'); + collabs_li.innerText = u + collabs_ul.appendChild(collabs_li); + } + } + + static refreshSummaryHosts(templateBlob) { + const hosts_ul = document.getElementById('booking_summary_hosts'); + hosts_ul.innerHTML = ''; + for (const h of templateBlob.host_list) { + const hosts_li = document.createElement('li'); + hosts_li.innerText = h.hostname; + hosts_ul.appendChild(hosts_li); + } + } +} + + + // Search widget for django forms (taken from dashboard.js and slightly modified) + class SearchableSelectMultipleWidget { + constructor(format_vars, field_dataset, field_initial) { + this.format_vars = format_vars; + this.items = field_dataset; + this.initial = field_initial; + + this.expanded_name_trie = {"isComplete": false}; + this.small_name_trie = {"isComplete": false}; + this.string_trie = {"isComplete": false}; + + this.added_items = new Set(); + + for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] ) + { + this[e] = format_vars[e]; + } + + this.search_field_init(); + + if( this.show_from_noentry ) + { + this.search(""); + } + } + + disable() { + const textfield = document.getElementById("user_field"); + const drop = document.getElementById("drop_results"); + + textfield.disabled = "True"; + drop.style.display = "none"; + + const btns = document.getElementsByClassName("btn-remove"); + for( const btn of btns ) + { + btn.classList.add("disabled"); + btn.onclick = ""; + } + } + + search_field_init() { + this.build_all_tries(this.items); + + for( const elem of this.initial ) + { + this.select_item(elem); + } + if(this.initial.length == 1) + { + this.search(this.items[this.initial[0]]["small_name"]); + document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"]; + } + } + + build_all_tries(dict) + { + for( const key in dict ) + { + this.add_item(dict[key]); + } + } + + add_item(item) + { + const id = item['id']; + this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie); + this.add_to_tree(item['small_name'], id, this.small_name_trie); + this.add_to_tree(item['string'], id, this.string_trie); + } + + add_to_tree(str, id, trie) + { + let inner_trie = trie; + while( str ) + { + if( !inner_trie[str.charAt(0)] ) + { + var new_trie = {}; + inner_trie[str.charAt(0)] = new_trie; + } + else + { + var new_trie = inner_trie[str.charAt(0)]; + } + + if( str.length == 1 ) + { + new_trie.isComplete = true; + if( !new_trie.ids ) + { + new_trie.ids = []; + } + new_trie.ids.push(id); + } + inner_trie = new_trie; + str = str.substring(1); + } + } + + search(input) + { + if( input.length == 0 && !this.show_from_noentry){ + this.dropdown([]); + return; + } + else if( input.length == 0 && this.show_from_noentry) + { + this.dropdown(this.items); //show all items + } + else + { + const trees = [] + const tr1 = this.getSubtree(input, this.expanded_name_trie); + trees.push(tr1); + const tr2 = this.getSubtree(input, this.small_name_trie); + trees.push(tr2); + const tr3 = this.getSubtree(input, this.string_trie); + trees.push(tr3); + const results = this.collate(trees); + this.dropdown(results); + } + } + + getSubtree(input, given_trie) + { + /* + recursive function to return the trie accessed at input + */ + + if( input.length == 0 ){ + return given_trie; + } + + else{ + const substr = input.substring(0, input.length - 1); + const last_char = input.charAt(input.length-1); + const subtrie = this.getSubtree(substr, given_trie); + + if( !subtrie ) //substr not in the trie + { + return {}; + } + + const indexed_trie = subtrie[last_char]; + return indexed_trie; + } + } + + serialize(trie) + { + /* + takes in a trie and returns a list of its item id's + */ + let itemIDs = []; + if ( !trie ) + { + return itemIDs; //empty, base case + } + for( const key in trie ) + { + if(key.length > 1) + { + continue; + } + itemIDs = itemIDs.concat(this.serialize(trie[key])); + } + if ( trie.isComplete ) + { + itemIDs.push(...trie.ids); + } + + return itemIDs; + } + + collate(trees) + { + /* + takes a list of tries + returns a list of ids of objects that are available + */ + const results = []; + for( const tree of trees ) + { + const available_IDs = this.serialize(tree); + + for( const itemID of available_IDs ) { + results[itemID] = this.items[itemID]; + } + } + return results; + } + + generate_element_text(obj) + { + const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x)); + const result = content_strings.shift(); + if( result == null || content_strings.length < 1) { + return result; + } else { + return result + " (" + content_strings.join(", ") + ")"; + } + } + + dropdown(ids) + { + /* + takes in a mapping of ids to objects in items + and displays them in the dropdown + */ + const drop = document.getElementById("drop_results"); + while(drop.firstChild) + { + drop.removeChild(drop.firstChild); + } + + for( const id in ids ) + { + const obj = this.items[id]; + const result_text = this.generate_element_text(obj); + const result_entry = document.createElement("a"); + result_entry.href = "#"; + result_entry.innerText = result_text; + result_entry.title = result_text; + result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0"); + result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); }; + const tooltip = document.createElement("span"); + const tooltiptext = document.createTextNode(result_text); + tooltip.appendChild(tooltiptext); + tooltip.classList.add("d-none"); + result_entry.appendChild(tooltip); + drop.appendChild(result_entry); + } + + const scroll_restrictor = document.getElementById("scroll_restrictor"); + + if( !drop.firstChild ) + { + scroll_restrictor.style.visibility = 'hidden'; + } + else + { + scroll_restrictor.style.visibility = 'inherit'; + } + } + + select_item(item_id) + { + if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 ) + { + this.added_items.add(item_id); + } + this.update_selected_list(); + // clear search bar contents + document.getElementById("user_field").value = ""; + document.getElementById("user_field").focus(); + this.search(""); + + const item = this.items[item_id]; + const element_entry_text = this.generate_element_text(item); + const username = item.small_name; + workflow.add_collaborator(username, element_entry_text); + } + + remove_item(item_id) + { + this.added_items.delete(item_id); // delete from set + + const item = this.items[item_id]; + workflow.remove_collaborator(item.small_name); + + this.update_selected_list(); + document.getElementById("user_field").focus(); + } + + update_selected_list() + { + document.getElementById("added_number").innerText = this.added_items.size; + const selector = document.getElementById('selector'); + selector.value = JSON.stringify([...this.added_items]); + const added_list = document.getElementById('added_list'); + + while(selector.firstChild) + { + selector.removeChild(selector.firstChild); + } + while(added_list.firstChild) + { + added_list.removeChild(added_list.firstChild); + } + + const list_html = document.createElement("div"); + list_html.classList.add("list-group"); + + for( const item_id of this.added_items ) + { + const times = document.createElement("li"); + times.classList.add("fas", "fa-times"); + + const deleteButton = document.createElement("a"); + deleteButton.href = "#"; + deleteButton.innerHTML = "<i class='fas fa-times'></i>" + deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`); + deleteButton.classList.add("btn"); + const deleteColumn = document.createElement("div"); + deleteColumn.classList.add("col-auto"); + deleteColumn.append(deleteButton); + + const item = this.items[item_id]; + const element_entry_text = this.generate_element_text(item); + const textColumn = document.createElement("div"); + textColumn.classList.add("col", "overflow-ellipsis"); + textColumn.innerText = element_entry_text; + textColumn.title = element_entry_text; + textColumn.id = `coldel-${item_id}`; // Needed for book a pod + + const itemRow = document.createElement("div"); + itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center", "my-2", "border"); + itemRow.append(textColumn, deleteColumn); + + list_html.append(itemRow); + } + added_list.innerHTML = list_html.innerHTML; + } +}
\ No newline at end of file diff --git a/src/static/js/workflows/common-models.js b/src/static/js/workflows/common-models.js new file mode 100644 index 0000000..65fedb1 --- /dev/null +++ b/src/static/js/workflows/common-models.js @@ -0,0 +1,189 @@ +/* +common-models.js +Defines classes used by the workflows +Functions as the "model" part of MVC +*/ + +// Provided by the LibLaaS API +// TemplateBlob classes +class TemplateBlob { + constructor(incomingBlob) { + this.id = incomingBlob.id; // UUID (String) + this.owner = incomingBlob.owner; // String + this.lab_name = incomingBlob.lab_name; // String + this.pod_name = incomingBlob.pod_name; // String + this.pod_desc = incomingBlob.pod_desc; // String + this["public"] = incomingBlob["public"]; // bool + this.host_list = []; // List<HostConfigBlob> + this.networks = []; // List<NetworkBlob> + + if (incomingBlob.host_list) { + this.host_list = incomingBlob.host_list; + } + + if (incomingBlob.networks) { + this.networks = incomingBlob.networks; + } + } + + /** + * Takes a network name (string) and returns the network stored in the template, or null if it does not exist + * @param {String} network_name + */ + findNetwork(network_name) { + for (const network of this.networks) { + if (network.name == network_name) { + return network; + } + } + + // Did not find it + return null; + } + + + /** + * Takes a hostname (string) and returns the host stored in the template, or null if it does not exist + * @param {String} hostname + */ + findHost(hostname) { + for (const host of this.host_list) { + if (host.hostname == hostname) { + return host; + } + } + + // Did not find it + return null; + } +} + +class HostConfigBlob { + constructor(incomingBlob) { + this.hostname = incomingBlob.hostname; // String + this.flavor = incomingBlob.flavor; // UUID (String) + this.image = incomingBlob.image; // UUID (String) + this.cifile = []; // List<String> + this.bondgroups = []; // List<BondgroupBlob> + + if (incomingBlob.cifile) { + this.cifile = incomingBlob.cifile; + } + + if (incomingBlob.bondgroups) { + this.bondgroups = incomingBlob.bondgroups; + } + } +} + +class NetworkBlob { + constructor(incomingBlob) { + this.name = incomingBlob.name; + this['public'] = incomingBlob['public']; + + } +} + +/** One bondgroup per interface at this time. */ +class BondgroupBlob { + constructor(incomingBlob) { + this.connections = []; //List<ConnectionBlob> + this.ifaces = []; // List<IfaceBlob> (will only contain the one iface for now) + + if (incomingBlob.connections) { + this.connections = incomingBlob.connections; + } + + if (incomingBlob.ifaces) { + this.ifaces = incomingBlob.ifaces; + } + } + +} + +class ConnectionBlob { + constructor(incomingBlob) { + this.tagged = incomingBlob.tagged; // bool, + this.connects_to = incomingBlob.connects_to; // String + } +} + +class InterfaceBlob { + constructor(incomingBlob) { + this.name = incomingBlob.name; // String, + this.speed = incomingBlob.speed; + this.cardtype = incomingBlob.cardtype; + } +} + +// BookingClasses +class BookingBlob { + // constructor({template_id, allowed_users, global_cifile}) { + constructor(incomingBlob) { + + this.template_id = incomingBlob.template_id; // UUID (String) + this.allowed_users = []; // List<String> + this.global_cifile = ""; // String + this.metadata = new BookingMetaDataBlob({}); + + if (incomingBlob.allowed_users) { + this.allowed_users = incomingBlob.allowed_users; + } + + if (incomingBlob.global_cifile) { + this.global_cifile = incomingBlob.global_cifile; + } + + if (incomingBlob.metadata) { + this.metadata = incomingBlob.metadata; + } + } +} + + +class BookingMetaDataBlob { + constructor(incomingBlob) { + this.booking_id = incomingBlob.booking_id; // String + this.owner = incomingBlob.owner; // String + this.lab = incomingBlob.lab; // String + this.purpose = incomingBlob.purpose; // String + this.project = incomingBlob.project; // String + this.length = 1 // Number + + if (incomingBlob.length) { + this.length = incomingBlob.length; + } + } +} + +// Utility Classes +class ImageBlob { + constructor(incomingBlob) { + this.image_id = incomingBlob.image_id; // UUID (String) + this.name = incomingBlob.name; // String, + } +} + +class FlavorBlob { + constructor(incomingBlob) { + this.flavor_id = incomingBlob.flavor_id; // UUID (String) + this.name = incomingBlob.name; // String + this.interfaces = []; // List<String> + // images are added after + + if (incomingBlob.interfaces) { + this.interfaces = incomingBlob.interfaces; + } + } + +} + +class LabBlob { + constructor(incomingBlob) { + this.name = incomingBlob.name; // String + this.description = incomingBlob.description; // String + this.location = incomingBlob.location; //String + this.status = incomingBlob.status; // Number + + } +}
\ No newline at end of file diff --git a/src/static/js/workflows/design-a-pod.js b/src/static/js/workflows/design-a-pod.js new file mode 100644 index 0000000..7632537 --- /dev/null +++ b/src/static/js/workflows/design-a-pod.js @@ -0,0 +1,1186 @@ +/* +design-a-pod.js + +Functions as the "controller" part of MVC +*/ + + +const steps = { + SELECT_LAB: 0, + ADD_RESOURCES: 1, + ADD_NETWORKS: 2, + CONFIGURE_CONNECTIONS: 3, + POD_DETAILS: 4, + POD_SUMMARY: 5 +} + +/** Concrete controller class that handles button inputs from the user. + * Holds the in-progress TemplateBlob. + * Click methods are prefaced with 'onclick'. + * Step initialization methods are prefaced with 'step'. + */ +class DesignWorkflow extends Workflow { + constructor(savedTemplateBlob) { + super(["select_lab", "add_resources", "add_networks", "configure_connections", "pod_details", "pod_summary"]) + + // if(savedTemplateBlob) { + // this.resume_workflow(); + // } + + this.templateBlob = new TemplateBlob({}); + this.labFlavors; // Map<UUID, FlavorBlob> + this.userTemplates; // List<TemplateBlob> + this.resourceBuilder; // ResourceBuilder + + this.templateBlob.public = false; + } + + /** Finds the templateBlob object in the userTemplates list based on a given uuid */ + getTemplateById(template_id) { + for (let template of this.userTemplates) { + if (template.id == template_id) { + return template; + } + } + return null; + } + + resumeWorkflow() { + todo() + } + + /** Initializes the select_lab step */ + async startWorkflow() { + this.setPodDetailEventListeners(); + const labs = await LibLaaSAPI.getLabs(); + GUI.display_labs(labs); + document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'}); + } + + /** Adds the public network on start */ + addDefaultNetwork() { + const new_network = new NetworkBlob({}); + new_network.name = "public"; + new_network.public = true; + this.addNetworkToPod(new_network); + GUI.refreshNetworkStep(this.templateBlob.networks); + } + + /** Takes an HTML element */ + async onclickSelectLab(lab_card) { + this.step = steps.SELECT_LAB; + + if (this.templateBlob.lab_name == null) { // Lab has not been selected yet + this.templateBlob.lab_name = lab_card.id; + lab_card.classList.add("selected_node"); + await this.setLabDetails(this.templateBlob.lab_name); + this.addDefaultNetwork(); + } else { // Lab has been selected + if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) { + location.reload(); + } + } + } + + /** Calls the API to fetch flavors and images for a lab */ + async setLabDetails(lab_name) { + const flavorsList = await LibLaaSAPI.getLabFlavors(lab_name); + this.labFlavors = new Map(); // Map<UUID, FlavorBlob> + this.labImages = new Map(); // Map<UUID, ImageBlob> + + for (const fblob of flavorsList) { + fblob.images = await LibLaaSAPI.getImagesForFlavor(fblob.flavor_id); + for (const iblob of fblob.images) { + this.labImages.set(iblob.image_id, iblob) + } + this.labFlavors.set(fblob.flavor_id, fblob); + } + + this.userTemplates = await LibLaaSAPI.getTemplatesForUser(); + } + + /** Prepopulates fields and launches the modal */ + onclickAddResource() { + // Set step + // Check prerequisites + // Reset resourceBuilder + // Generate template cards + // Show modal + + this.step = steps.ADD_RESOURCES; + + if (this.templateBlob.lab_name == null) { + alert("Please select a lab before adding resources."); + this.goTo(steps.SELECT_LAB); + return; + } + + if (this.templateBlob.host_list.length >= 8) { + alert("You may not add more than 8 hosts to a single pod.") + return; + } + + this.resourceBuilder = null; + GUI.refreshAddHostModal(this.userTemplates); + $("#resource_modal").modal('toggle'); + + } + + onclickSelectTemplate(template_id, card) { + + // Do nothing on reselect + if (this.resourceBuilder && this.resourceBuilder.template_id == template_id) { + return; + } + + if (this.resourceBuilder) { + GUI.unhighlightCard(document.querySelector('#template-cards .selected_node')); + } + + this.resourceBuilder = new ResourceBuilder(this.getTemplateById(template_id)); + GUI.highlightCard(card); + GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); + GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); + } + + onclickSelectNode(index) { + this.resourceBuilder.tab = index; + GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); + } + + onclickSelectImage(image_id, card) { + const old_selection = document.querySelector("#image-cards .selected_node"); + if (old_selection) { + GUI.unhighlightCard(old_selection); + } + this.resourceBuilder.user_configs[this.resourceBuilder.tab].image = image_id; + GUI.highlightCard(card.childNodes[1]); + } + + /** Takes a string and returns a tuple containing the result and the error message (bool, string)*/ + isValidHostname(hostname) { + let result = true; + let message = "success"; + + if (hostname == null || hostname == '') { + result = false; + message = 'Please enter a hostname'; + + } else if (hostname.length > 25) { + result = false; + message = 'Hostnames cannot exceed 25 characters'; + + } else if (!(hostname.match(/^[0-9a-z-]+$/i))) { + result = false; + message = 'Hostnames must only contain alphanumeric characters and dashes'; + + } else if ((hostname.charAt(0).match(/^[0-9-]+$/)) || (hostname.charAt(hostname.length - 1) == '-')) { + result = false; + message = 'Hostnames must start with a letter and end with a letter or digit.'; + } + + return [result, message]; + } + + /** Takes a hostname and a list of existing hosts and checks for duplicates in the existing hostlist*/ + isUniqueHostname(hostname, existing_hosts) { + for (const existing_host of existing_hosts) { + if (hostname == existing_host.hostname) { + return false; + } + } + + return true; + } + + onclickSubmitHostConfig() { + // Validate form fields + // Create host config blobs + // Apply networking + // Create cards (refresh hostcard view) + // Refresh networks view + // Refresh connections view + + // Validate Configs + for (const [index, host] of this.resourceBuilder.user_configs.entries()) { + let result = this.isValidHostname(host.hostname); + if (!result[0]) { + this.resourceBuilder.tab = index; + GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); + GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); + GUI.showHostConfigErrorMessage(result[1]); + return; + } + + let uniqueHost = this.isUniqueHostname(host.hostname, this.templateBlob.host_list); + if (!uniqueHost) { + this.resourceBuilder.tab = index; + GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); + GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); + GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' already exists in Pod."); + return; + } + + if (index < this.resourceBuilder.user_configs.length - 1) { + let uniqueConfigName = true; + for (let i = index + 1; i < this.resourceBuilder.user_configs.length; i++) { + if (host.hostname == this.resourceBuilder.user_configs[i].hostname) { + uniqueConfigName = false; + break; + } + } + + if (!uniqueConfigName) { + this.resourceBuilder.tab = index; + GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); + GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); + GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' is a duplicate hostname."); + return; + } + } + + // todo + // let result2 = isValidCIFile(host.cifile[0]); + } + + + // Add host configs to TemplateBlob + for (const [index, host] of this.resourceBuilder.user_configs.entries()) { + const new_host = new HostConfigBlob(host); + this.templateBlob.host_list.push(new_host); + } + + // Add networks + for (const n of this.resourceBuilder.networks) { + if (!this.templateBlob.findNetwork(n.name)) { + this.templateBlob.networks.push(n); + } + } + + // We are done + GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages); + GUI.refreshNetworkStep(this.templateBlob.networks); + GUI.refreshConnectionStep(this.templateBlob.host_list); + GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages) + $('#resource_modal').modal('hide') + } + + /** + * Takes a hostname, looks for the matching HostConfigBlob in the TemplateBlob, removes it from the list, and refreshes the appropriate views + * @param {String} hostname + */ + onclickDeleteHost(hostname) { + this.step = steps.ADD_RESOURCES; + for (let existing_host of this.templateBlob.host_list) { + if (hostname == existing_host.hostname) { + this.removeHostFromTemplateBlob(existing_host); + GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages); + GUI.refreshNetworkStep(this.templateBlob.networks); + GUI.refreshConnectionStep(this.templateBlob.host_list); + GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages); + return; + } + } + + alert("didnt remove"); + } + + + + + /** onclick handler for the add_network_plus_card */ + onclickAddNetwork() { + // Set step + // Prerequisite step checks + // GUI stuff + + this.step = steps.ADD_NETWORKS; + + if (this.templateBlob.lab_name == null) { + alert("Please select a lab before adding networks."); + this.goTo(steps.SELECT_LAB); + return; + } + + if (document.querySelector('#new_network_card') != null) { + alert("Please finish adding the current network before adding a new one."); + return; + } + + GUI.display_network_input(); + } + + /** onclick handler for the adding_network_confirm button */ + onclickConfirmNetwork() { + this.step = steps.ADD_NETWORKS; + + // Add the network + // call the GUI to make the card (refresh the whole view to make it easier) + + const new_network = new NetworkBlob({}); + new_network.name = document.getElementById('network_input').value; + new_network.public = document.getElementById('network-public-input').checked; + const error_message = this.addNetworkToPod(new_network); + + if (error_message == null) { + GUI.refreshNetworkStep(this.templateBlob.networks); + GUI.refreshConnectionStep(this.templateBlob.host_list); + } else { + GUI.display_add_network_error(error_message); + } + } + + /** Takes a NetworkBlob and tries to add to the TemplateBlob. + * Fails if input validation fails. + * Returns error message or null. + */ + addNetworkToPod(networkBlob) { + if (networkBlob.name == '' || networkBlob.name == null) { + return "Network name cannot be empty."; + } + + if (networkBlob.name.length > 25) { + return 'Network names cannot exceed 25 characters'; + } + + if (!(networkBlob.name.match(/^[0-9a-z-]+$/i))) { + return 'Network names must only contain alphanumeric characters and dashes'; + } + + if ((networkBlob.name.charAt(0).match(/^[0-9-]+$/)) || (networkBlob.name.charAt(networkBlob.name.length - 1) == '-')) { + return 'Network names must start with a letter and end with a letter or digit.'; + } + + for (let existing_network of this.templateBlob.networks) { + if (networkBlob.name == existing_network.name) { + return 'Networks must have unique names'; + } + } + + this.templateBlob.networks.push(networkBlob); + return null; + } + + /** Iterates through the templateBlob looking for the correct network to delete + * Takes a network name as a parameter. + */ + onclickDeleteNetwork(network_name) { + this.step = steps.ADD_NETWORKS; + + for (let existing_network of this.templateBlob.networks) { + if (network_name == existing_network.name) { + this.removeNetworkFromTemplateBlob(existing_network); + this.removeConnectionsOnNetwork(existing_network.name) + GUI.refreshNetworkStep(this.templateBlob.networks); + GUI.refreshConnectionStep(this.templateBlob.host_list); + return; + } + } + + alert("didnt remove"); + } + + /** Rebuilds the list without the chosen template */ + removeNetworkFromTemplateBlob(network_to_remove) { + this.templateBlob.networks = this.templateBlob.networks.filter(network => network !== network_to_remove); + } + + removeConnectionsOnNetwork(network_name) { + for (const host of this.templateBlob.host_list) { + for (const bg of host.bondgroups) { + bg.connections = bg.connections.filter((connection) => connection.connects_to != network_name) + } + } + } + + /** + * Rebuilds the hostlist without the chosen host + * Also removes all connections from this host's interfaces + * @param {HostConfigBlob} hostBlob + */ + removeHostFromTemplateBlob(hostBlob) { + this.templateBlob.host_list = this.templateBlob.host_list.filter(host => host !== hostBlob); + } + + onclickConfigureConnection(hostname) { + this.step = steps.CONFIGURE_CONNECTIONS; + + const host = this.templateBlob.findHost(hostname); + if (!host) { + alert("host not found error"); + } + + this.connectionTemp = new ConnectionTemp(host, this.templateBlob.networks, this.labFlavors.get(host.flavor).interfaces); + GUI.refreshConnectionModal(this.connectionTemp); + $("#connection_modal").modal('toggle'); + } + + onclickSelectIfaceTab(tab_index) { + this.connectionTemp.selected_index = tab_index; + GUI.refreshConnectionModal(this.connectionTemp); + } + + onclickSelectVlan(network_name, tagged, iface_name) { + const x = this.connectionTemp.config.get(iface_name); + if (x.get(network_name) === tagged) { + x.set(network_name, null); + } else { + x.set(network_name, tagged); + } + + GUI.refreshConnectionTable(this.connectionTemp); + } + + onclickSubmitConnectionConfig() { + this.connectionTemp.applyConfigs(); + GUI.refreshConnectionStep(this.templateBlob.host_list); + } + + /** Sets input validation event listeners and clears the value in case of caching*/ + setPodDetailEventListeners() { + const pod_name_input = document.getElementById("pod-name-input"); + const pod_desc_input = document.getElementById("pod-desc-input"); + const pod_public_input = document.getElementById("pod-public-input"); + + pod_name_input.value = ""; + pod_desc_input.value = ""; + pod_public_input.checked = false; + + pod_name_input.addEventListener('focusout', (event)=> { + workflow.onFocusOutPodNameInput(pod_name_input); + }); + + pod_name_input.addEventListener('focusin', (event)=> { + this.step = steps.POD_DETAILS; + GUI.unhighlightError(pod_name_input); + GUI.hidePodDetailsError(); + }); + + pod_desc_input.addEventListener('focusout', (event)=> { + workflow.onFocusOutPodDescInput(pod_desc_input); + }); + + pod_desc_input.addEventListener('focusin', (event)=> { + this.step = steps.POD_DETAILS; + GUI.unhighlightError(pod_desc_input); + GUI.hidePodDetailsError(); + }); + + pod_public_input.addEventListener('focusout', (event)=> { + this.step = steps.POD_DETAILS; + workflow.onFocusOutPodPublicInput(pod_public_input); + }); + } + + onFocusOutPodNameInput(element) { + const pod_name = element.value; + const validator = this.validatePodInput(pod_name, 53, "Pod name"); + + if (validator[0]) { + this.templateBlob.pod_name = pod_name; + GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) + } else { + GUI.highlightError(element); + GUI.showPodDetailsError(validator[1]); + } + } + + onFocusOutPodDescInput(element) { + const pod_desc = element.value; + const validator = this.validatePodInput(pod_desc, 255, "Pod description"); + + if (validator[0]) { + this.templateBlob.pod_desc = pod_desc; + GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) + } else { + GUI.highlightError(element); + GUI.showPodDetailsError(validator[1]); + } + + } + + onFocusOutPodPublicInput(element) { + this.templateBlob.public = element.checked; + GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) + } + + /** Returns a tuple containing result and message (bool, String) */ + validatePodInput(input, maxCharCount, form_name) { + let result = true; + let message = "valid" + + if (input === '') { + message = form_name + ' cannot be empty.'; + result = false; + } + else if (input.length > maxCharCount) { + message = form_name + ' cannot exceed ' + maxCharCount + ' characters.'; + result = false; + } else if (!(input.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -!]+$/i))) { + message = form_name + ' contains invalid characters.'; + result = false; + } + + return [result, message] + } + + async onclickDiscardTemplate() { + this.step = steps.POD_SUMMARY; + if(confirm('Are you sure you wish to delete this Pod?')) { + await LibLaaSAPI.deleteTemplate(this.templateBlob); + location.reload(); + } + } + + simpleStepValidation() { + let passed = true; + let message = "done"; + let step = steps.POD_SUMMARY; + + if (this.templateBlob.lab_name == null) { + passed = false; + message = "Please select a lab"; + step = steps.SELECT_LAB; + } else if (this.templateBlob.host_list.length < 1 || this.templateBlob.host_list.length > 8) { + passed = false; + message = "Pods must contain 1 to 8 hosts"; + step = steps.ADD_RESOURCES; + } else if (this.templateBlob.networks.length < 1) { + passed = false; + message = "Pods must contain at least one network."; + step = steps.ADD_NETWORKS; + } else if (this.templateBlob.pod_name == null || this.templateBlob.pod_desc == null) { + passed = false; + message = "Please add a valid pod name and description."; + step = steps.POD_DETAILS; + } + return [passed, message, step]; + } + + async onclickSubmitTemplate() { + this.step = steps.POD_SUMMARY; + const simpleValidation = this.simpleStepValidation(); + if (!simpleValidation[0]) { + alert(simpleValidation[1]) + this.goTo(simpleValidation[2]); + return; + } + + // todo - make sure each host has at least one connection on any network. + + if (confirm("Are you sure you wish to create this pod?")) { + let success = await LibLaaSAPI.makeTemplate(this.templateBlob); + if (success) { + window.location.href = "../../accounts/my/resources/"; + } else { + alert("Could not create template.") + } + } + } +} + +/** View class that displays cards and generates HTML + * Functions as a namespace, does not hold state +*/ +class GUI { + /** Takes a list of LabBlobs and creates a card for each of them on the screen */ + static display_labs(labs) { + const card_deck = document.getElementById('lab_cards'); + for (let lab of labs) { + const new_col = document.createElement('div'); + new_col.classList.add('col-xl-3','col-md-6','col-11'); + let status; + if (lab.status == 0) { + status = "Up"; + } else if (lab.status == 100) { + status = "Down for Maintenance"; + } else if (lab.status == 200) { + status = "Down"; + } else { + status = "Unknown"; + } + + new_col.innerHTML = ` + <div class="card" id= ` + lab.name + `> + <div class="card-header"> + <h3 class="mt-2">` + lab.name + `</h3> + </div> + <ul class="list-group list-group-flush h-100"> + <li class="list-group-item">Name: ` + lab.name + `</li> + <li class="list-group-item">Description: ` + lab.description + `</li> + <li class="list-group-item">Location: ` + lab.location + `</li> + <li class="list-group-item">Status: `+ status + `</li> + </ul> + <div class="card-footer"> + <btn class="btn btn-success w-100 stretched-link" href="#" onclick="workflow.onclickSelectLab(this.parentNode.parentNode)">Select</btn> + </div> + </div> + ` + card_deck.appendChild(new_col); + } + } + + static highlightCard(card) { + card.classList.add('selected_node'); + } + + static unhighlightCard(card) { + card.classList.remove('selected_node'); + } + + /** Resets the host modal inner html + * Takes a list of templateBlobs + */ + static refreshAddHostModal(template_list) { + document.getElementById('add_resource_modal_body').innerHTML = ` + <h2>Resource</h2> + <div id="template-cards" class="row align-items-center justify-content-start"> + </div> + + <div id="template-config-section"> + <ul class="nav nav-tabs" role="tablist" id="add_resource_tablist"> + <!-- add a tab per host in template --> + </ul> + <!-- tabs --> + <div id="resource_config_section" hidden="true"> + <h2>Image</h2> + <div id="image-cards" class="row justify-content-start align-items-center"> + </div> + <div class="form-group"> + <h2>Hostname</h2> + <input type="text" class="form-control" id="hostname-input" placeholder="Enter Hostname"> + <h2>Cloud Init</h2> + <div class="d-flex justify-content-center align-items-center"> + <textarea name="ci-textarea" id="ci-textarea" rows="5" class="w-100"></textarea> + </div> + </div> + </div> + </div> + <p id="add-host-error-msg" class="text-danger"></p> + ` + + const template_cards = document.getElementById('template-cards'); + + for (let template of template_list) { + template_cards.appendChild(this.makeTemplateCard(template)); + } + } + + + /** Makes a card to be displayed in the add resource modal for a given templateBlob */ + static makeTemplateCard(templateBlob) { + const col = document.createElement('div'); + col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3'); + col.innerHTML= ` + <div class="card" id="card-" ` + templateBlob.id + `> + <div class="card-header"> + <p class="h5 font-weight-bold mt-2">` + templateBlob.pod_name + `</p> + </div> + <div class="card-body"> + <p class="grid-item-description">` + templateBlob.pod_desc +`</p> + </div> + <div class="card-footer"> + <button type="button" class="btn btn-success grid-item-select-btn w-100 stretched-link" + onclick="workflow.onclickSelectTemplate('` + templateBlob.id + `', this.parentNode.parentNode)">Select</button> + </div> + </div> + ` + return col; + } + + /** Takes a ResourceBuilder and generates form fields */ + static refreshConfigSection(resourceBuilder, flavors) { + // Create a tab for head host in the selected template + const tablist = document.getElementById('add_resource_tablist'); // ul + tablist.innerHTML = ""; + for (const [index, host] of resourceBuilder.user_configs.entries()) { + const li_interface = document.createElement('li'); + li_interface.classList.add('nav-item'); + const btn_interface = document.createElement('a'); + btn_interface.classList.add('nav-link', 'interface-btn'); + btn_interface.id = "select-node-" + index; + btn_interface.setAttribute("onclick", "workflow.onclickSelectNode("+ index + ")"); + btn_interface.setAttribute('href', "#"); + btn_interface.setAttribute('role', 'tab'); + btn_interface.setAttribute('data-toggle', 'tab'); + btn_interface.innerText = flavors.get(host.flavor).name; + + if (index == resourceBuilder.tab) { + btn_interface.classList.add('active'); + } + li_interface.appendChild(btn_interface); + tablist.appendChild(li_interface); + } + } + + static refreshInputSection(resourceBuilder, flavor_map) { + // config stuff + const image_cards = document.getElementById('image-cards'); + const hostname_input = document.getElementById('hostname-input'); + const ci_textarea = document.getElementById('ci-textarea'); + + const tab_flavor_id = resourceBuilder.original_configs[resourceBuilder.tab].flavor; + const tab_flavor = flavor_map.get(tab_flavor_id); + const image_list = tab_flavor.images; + image_cards.innerHTML = ""; + for (let imageBlob of image_list) { + const new_image_card = this.makeImageCard(imageBlob); + new_image_card.setAttribute("onclick", "workflow.onclickSelectImage('" + imageBlob.image_id + "', this)"); + if (resourceBuilder.user_configs[resourceBuilder.tab].image == imageBlob.image_id) { + GUI.highlightCard(new_image_card.childNodes[1]); + } + image_cards.appendChild(new_image_card); + } + + // Hostname input + hostname_input.value = resourceBuilder.user_configs[resourceBuilder.tab].hostname; + hostname_input.addEventListener('focusout', (event)=> { + resourceBuilder.user_configs[resourceBuilder.tab].hostname = hostname_input.value; + }); + + hostname_input.addEventListener('focusin', (event)=> { + this.removeHostConfigErrorMessage(); + }); + + // CI input + let ci_value = resourceBuilder.user_configs[resourceBuilder.tab].cifile[0]; + if (!ci_value) { + ci_value = ""; + } + ci_textarea.value = ci_value; + ci_textarea.addEventListener('focusout', (event)=> { + resourceBuilder.user_configs[resourceBuilder.tab].cifile[0] = ci_textarea.value; + }) + this.removeHostConfigErrorMessage(); + document.getElementById('resource_config_section').removeAttribute('hidden'); + } + + static showHostConfigErrorMessage(message) { + document.getElementById("hostname-input").classList.add("invalid_field"); + document.getElementById('add-host-error-msg').innerText = message; + } + + static removeHostConfigErrorMessage() { + document.getElementById("hostname-input").classList.remove("invalid_field"); + document.getElementById('add-host-error-msg').innerText = ""; + } + + static makeImageCard(imageBlob) { + const col = document.createElement('div'); + col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3'); + col.innerHTML = ` + <div class="btn border w-100">` + imageBlob.name +`</div> + ` + + return col; + } + + static highlightError(element) { + element.classList.add('invalid_field'); + } + + static unhighlightError(element) { + element.classList.remove("invalid_field"); + } + + static showPodDetailsError(message) { + document.getElementById('pod_details_error').innerText = message; + } + + static hidePodDetailsError() { + document.getElementById('pod_details_error').innerText = "" + } + + /** + * Refreshes the step and creates a card for each host in the hostlist + * @param {List<HostConfigBlob>} hostlist + */ + static refreshHostStep(hostlist, flavors, images) { + const host_cards = document.getElementById('host_cards'); + host_cards.innerHTML = ""; + for (const host of hostlist) { + host_cards.appendChild(this.makeHostCard(host, flavors, images)); + } + + let span_class = '' + if (hostlist.length == 8) { + span_class = 'text-primary' + } else if (hostlist.length > 8) { + span_class = 'text-danger' + } + const plus_card = document.createElement("div"); + plus_card.classList.add("col-xl-3", "col-md-6", "col-12"); + plus_card.id = "add_resource_plus_card"; + plus_card.innerHTML = ` + <div class="card align-items-center border-0"> + <span class="` + span_class + `" id="resource-count">` + hostlist.length + `/ 8</span> + <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddResource()">+</button> + </div> + ` + + host_cards.appendChild(plus_card); + } + + /** + * Makes a host card element for a given host and returns a reference to the card + * @param {HostConfigBlob} host + */ + static makeHostCard(host, flavors, images) { + const new_card = document.createElement("div"); + new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3"); + new_card.innerHTML = ` + <div class="card"> + <div class="card-header"> + <h3 class="mt-2">` + flavors.get(host.flavor).name + `</h3> + </div> + <ul class="list-group list-group-flush h-100"> + <li class="list-group-item">Hostname: ` + host.hostname + `</li> + <li class="list-group-item">Image: ` + images.get(host.image).name + `</li> + </ul> + <div class="card-footer border-top-0"> + <button class="btn btn-danger w-100" id="delete-host-` + host.hostname + `" onclick="workflow.onclickDeleteHost('` + host.hostname +`')">Delete</button> + </div> + </div> + `; + + return new_card; + } + + + /** Shows the input card for adding a new network */ + // Don't forget to redisable + static display_network_input() { + // New empty card + const network_plus_card = document.getElementById('add_network_plus_card'); + const new_card = document.createElement('div'); + new_card.classList.add("col-xl-3", "col-md-6","col-12"); + new_card.innerHTML = + `<div class="card pb-0" id="new_network_card"> + <div class="card-body pb-0"> + <div class="justify-content-center my-5 mx-2"> + <input type="text" class="form-control col-12 mb-2 text-center" id="network_input" style="font-size: 1.75rem;" placeholder="Enter Network Name"> + <div class="custom-control custom-switch"> + <input type="checkbox" class="custom-control-input" id="network-public-input"> + <label class="custom-control-label" for="network-public-input">public?</label> + </div> + </br> + <p class="text-danger mt-n2" id="adding_network_error"></p> + </div> + <div class="row mb-3"> + <div class="col-6"><button class="btn btn-danger w-100" onclick="GUI.hide_network_input()">Delete</button></div> + <div class="col-6"><button class="btn btn-success w-100" id="adding_network_confirm" onclick="workflow.onclickConfirmNetwork()">Confirm</button></div> + </div> + </div> + </div>`; + network_plus_card.parentNode.insertBefore(new_card, network_plus_card); + + document.getElementById('network_input').addEventListener('focusin', e => { + document.getElementById('adding_network_error').innerText = ''; + }) + } + + static hide_network_input() { + document.getElementById('new_network_card').parentElement.remove(); + document.getElementById('add_network_plus_card').hidden = false; + } + + /** Redraws all the cards on the network step. + * Takes a list of networks to display + */ + static refreshNetworkStep(network_list) { + + document.getElementById('network_card_deck').innerHTML = ` + <div class="col-xl-3 col-md-6 col-12" id="add_network_plus_card"> + <div class="card align-items-center border-0"> + <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddNetwork()">+</button> + </div> + </div> + ` + + const network_plus_card = document.getElementById('add_network_plus_card'); + for (let network of network_list) { // NetworkBlobs + + let pub_str = ' (private)'; + if (network.public) { + pub_str = ' (public)' + } + const new_card = document.createElement('div'); + new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3"); + new_card.innerHTML = ` + <div class="card"> + <div class="text-center"> + <h3 class="py-5 my-4">` + network.name + pub_str +`</h3> + </div> + <div class="row mb-3 mx-3"> + <button class="btn btn-danger w-100" id="delete_network_` + network.name + `" onclick="workflow.onclickDeleteNetwork('`+ network.name +`')">Delete</button> + </div> + </div>`; + network_plus_card.parentNode.insertBefore(new_card, network_plus_card); + } + } + + /** Displays an error message on the add network card */ + static display_add_network_error(error_message) { + document.getElementById("adding_network_error").innerHTML = error_message; + } + + static refreshConnectionStep(host_list) { + const connection_cards = document.getElementById('connection_cards'); + connection_cards.innerHTML = ""; + + for (const host of host_list) { + connection_cards.appendChild(this.makeConnectionCard(host)); + } + + } + + + /** Makes a blank connection card that does not contain interface details */ + static makeConnectionCard(host) { + const new_card = document.createElement('div'); + new_card.classList.add("col-xl-3", "col-md-6","col-11", "my-3"); + + const card_div = document.createElement('div'); + card_div.classList.add('card'); + new_card.appendChild(card_div); + + const card_header = document.createElement('div'); + card_header.classList.add('card-header', 'text-center', 'p-0'); + card_header.innerHTML = `<h3 class="mt-2">` + host.hostname + `</h3>` + + const card_body = document.createElement('div'); + card_body.classList.add('card-body', 'card-body-scroll', 'p-0'); + const bondgroup_list = document.createElement('ul'); + bondgroup_list.classList.add('list-group', 'list-group-flush', 'h-100') + card_body.appendChild(bondgroup_list) + + const card_footer = document.createElement('div'); + card_footer.classList.add('card-footer'); + card_footer.innerHTML = `<button class="btn btn-info w-100" onclick="workflow.onclickConfigureConnection('` + host.hostname + `')">Configure</button>` + + card_div.appendChild(card_header); + card_div.appendChild(card_body); + card_div.appendChild(card_footer); + + for (const bg of host.bondgroups) { + const outer_block = document.createElement('li'); + outer_block.classList.add('list-group-item'); + outer_block.innerHTML = ` + <h5>` + bg.ifaces[0].name + `</h5>` + + const inner_block = document.createElement('ul'); + inner_block.classList.add('connection-holder'); + outer_block.appendChild(inner_block) + for (const c of bg.connections) { + const connection_li = document.createElement('li'); + connection_li.innerText = c.connects_to + `: ` + c.tagged; + inner_block.appendChild(connection_li); + } + bondgroup_list.appendChild(outer_block) + } + + return new_card; + } + + /** */ + static refreshConnectionModal(connectionTemp) { + this.refreshConnectionTabs(connectionTemp.iface_list, connectionTemp.selected_index); + this.refreshConnectionTable(connectionTemp); + } + + /** Displays a tab in the connections modal for each interface + * Returns the name of the currently selected interface for use in the connections table + */ + static refreshConnectionTabs (iface_list, iface_index) { + const tablist_ul = document.getElementById('configure-connections-tablist'); + tablist_ul.innerHTML = ''; + for (const [index, iface_name] of iface_list.entries()) { + const li_interface = document.createElement('li'); + li_interface.classList.add('nav-item'); + const btn_interface = document.createElement('a'); + btn_interface.classList.add('nav-link', 'interface-btn'); + btn_interface.setAttribute("onclick", "workflow.onclickSelectIfaceTab(" + index +")"); + btn_interface.setAttribute('href', "#"); + btn_interface.setAttribute('role', 'tab'); + btn_interface.setAttribute('data-toggle', 'tab'); + btn_interface.innerText = iface_name.name; + li_interface.appendChild(btn_interface); + tablist_ul.appendChild(li_interface); + if (index == iface_index) { + btn_interface.classList.add('active'); + } + } + } + + static refreshConnectionTable(connectionTemp) { + const connections_table = document.getElementById('connections_widget'); + connections_table.innerHTML =` + <tr> + <th>Network</th> + <th colspan='2'>Vlan</th> + </tr> + `; + + const selected_iface_name = connectionTemp.iface_list[connectionTemp.selected_index].name; + const iface_config = connectionTemp.config.get(selected_iface_name); + for (const network of connectionTemp.networks) { + const tagged = iface_config.get(network.name); + const new_row = document.createElement('tr'); + const td_network = document.createElement('td'); + td_network.innerText = network.name; + new_row.appendChild(td_network); + new_row.appendChild(this.makeTagTd(true, network.name, tagged === true, selected_iface_name)); + new_row.appendChild(this.makeTagTd(false, network.name, tagged === false, selected_iface_name)); + connections_table.appendChild(new_row); + } + + // If an untagged is selected, disable all buttons that are not the selected button + if (document.querySelector(".vlan-radio.untagged.btn-success")) { + const other_buttons = document.querySelectorAll(".vlan-radio.untagged:not(.btn-success"); + for (const btn of other_buttons) { + btn.setAttribute("disabled", "true") + } + } + } + + static makeTagTd(tagged, network_name, isSelected, selected_iface_name) { + let tagged_as_str = "untagged" + if (tagged) { + tagged_as_str = "tagged" + } + + const td = document.createElement('td'); + const btn = document.createElement('button'); + btn.classList.add("btn", "w-100", "h-100", "vlan-radio", "border", tagged_as_str); + btn.setAttribute("onclick" ,"workflow.onclickSelectVlan('"+ network_name + "'," + tagged + ", '" + selected_iface_name +"')"); + if (isSelected) { + btn.classList.add('btn-success'); + } + btn.innerText = tagged_as_str; + td.appendChild(btn); + return td; + } + + static refreshPodSummaryDetails(pod_name, pod_desc, isPublic) { + const list = document.getElementById('pod_summary_pod_details'); + list.innerHTML = ''; + const name_li = document.createElement('li'); + name_li.innerText = 'Pod name: ' + pod_name; + list.appendChild(name_li); + + const desc_li = document.createElement('li') + desc_li.innerText = 'Description: ' + pod_desc; + list.appendChild(desc_li); + + const public_li = document.createElement('li'); + public_li.innerText = 'Public: ' + isPublic; + list.appendChild(public_li); + } + + static refreshPodSummaryHosts(host_list, flavors, images) { + const list = document.getElementById('pod_summary_hosts'); + list.innerHTML = ''; + + for (const host of host_list) { + const new_li = document.createElement('li'); + // new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')'; + const details = `${host.hostname}: ${flavors.get(host.flavor).name}, ${images.get(host.image).name}` + new_li.innerText = details; + list.appendChild(new_li); + } + } + + static update_pod_summary() { + // Takes a section (string) and updates the appropriate element's innertext + + if (section == 'pod_details') { + const list = document.getElementById('pod_summary_pod_details'); + list.innerHTML = ''; + const name_li = document.createElement('li'); + name_li.innerText = 'Pod name: ' + this.pod.pod_name; + list.appendChild(name_li); + + const desc_li = document.createElement('li') + desc_li.innerText = 'Description: ' + this.pod.pod_desc; + list.appendChild(desc_li); + + const public_li = document.createElement('li'); + public_li.innerText = 'Public: ' + this.pod.is_public; + list.appendChild(public_li); + } else if (section == 'hosts') { + const list = document.getElementById('pod_summary_hosts'); + list.innerHTML = ''; + const hosts = this.pod.host_list; + for (let i = 0; i < this.pod.host_list.length; i++) { + const new_li = document.createElement('li'); + new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')'; + list.appendChild(new_li); + } + } else { + console.log(section + ' is not a valid section.'); + } + } +} + +/** Holds in-memory configurations for the add resource step */ +class ResourceBuilder { + constructor(templateBlob) { + this.template_id = templateBlob.id; // UUID (String) + this.networks = templateBlob.networks; + this.original_configs = templateBlob.host_list; // List<HostConfigBlob> + this.user_configs = []; // List<HostConfigBlob> + this.tab = 0; // Currently selected tab index where configs will be saved to + + // Create deep copies of the hosts + for (let host of this.original_configs) { + const copied_host = new HostConfigBlob(host); + this.user_configs.push(copied_host); + } + } +} + +class ConnectionTemp { + // keep track of user inputs, commits to host bondgroups after user clicks submit + constructor(host, networks, iface_list) { + this.host = host; // reference + this.config = new Map(); // Map<iface_name, Map<network_name, tagged>}> + this.iface_list = iface_list; // List<IfaceBlob> + for (const i of iface_list) { + this.config.set(i.name, new Map()) + } + + // set initial mappings + for (const ebg of host.bondgroups) { + // if (ebg.ifaces[0].name) + const iface_config = this.config.get(ebg.ifaces[0].name); + for (const c of ebg.connections) { + iface_config.set(c.connects_to, c.tagged) + } + } + this.networks = networks; // List<NetworkBlob> + this.selected_index = 0; + } + + + /** Replaces the old configs in the hostconfigblob with the ones set in this.config */ + applyConfigs() { + this.host.bondgroups = []; + for (const [key, value] of this.config) { + if (value.size > 0) { + const full_iface = workflow.labFlavors.get(this.host.flavor).interfaces.filter((iface) => iface.name == key)[0]; + const new_bg = new BondgroupBlob({}); + this.host.bondgroups.push(new_bg) + new_bg.ifaces.push(full_iface); + for (const [network, tagged] of value) { + if (tagged != null) { + new_bg.connections.push(new ConnectionBlob({"tagged": tagged, "connects_to": network})) + } + } + } + } + } +} + +function todo() { + alert('todo'); +}
\ No newline at end of file diff --git a/src/static/js/workflows/workflow.js b/src/static/js/workflows/workflow.js new file mode 100644 index 0000000..745a706 --- /dev/null +++ b/src/static/js/workflows/workflow.js @@ -0,0 +1,246 @@ +/* +Defines a common interface for creating workflows +Functions as the "view" part of MVC, or the "controller" part. Not really sure tbh +*/ + + +const HTTP = { + GET: "GET", + POST: "POST", + DELETE: "DELETE", + PUT: "PUT" +} + +const endpoint = { + LABS: "todo", // Not implemented + FLAVORS: "flavor/", + IMAGES: "images/", + TEMPLATES: "template/list/[username]", + SAVE_DESIGN_WORKFLOW: "todo", // Post MVP + SAVE_BOOKING_WORKFLOW: "todo", // Post MVP + MAKE_TEMPLATE: "template/create", + DELETE_TEMPLATE: "template", + MAKE_BOOKING: "booking/create", +} + +/** Functions as a namespace for static methods that post to the dashboard, then send an HttpRequest to LibLaas, then receive the response */ +class LibLaaSAPI { + + /** POSTs to dashboard, which then auths and logs the requests, makes the request to LibLaaS, and passes the result back to here. + Treat this as a private function. Only use the async functions when outside of this class */ + static makeRequest(method, endpoint, workflow_data) { + console.log("Making request: %s, %s, %s", method, endpoint, workflow_data.toString()) + const token = document.getElementsByName('csrfmiddlewaretoken')[0].value + return new Promise((resolve, reject) => {// -> HttpResponse + $.ajax( + { + crossDomain: true, // might need to change this back to true + method: "POST", + contentType: "application/json; charset=utf-8", + dataType : 'json', + headers: { + 'X-CSRFToken': token + }, + data: JSON.stringify( + { + "method": method, + "endpoint": endpoint, + "workflow_data": workflow_data + } + ), + timeout: 10000, + success: (response) => { + resolve(response); + }, + error: (response) => { + reject(response); + } + } + ) + }) + } + + static async getLabs() { // -> List<LabBlob> + // return this.makeRequest(HTTP.GET, endpoint.LABS, {}); + let jsonObject = JSON.parse('{"name": "UNH_IOL","description": "University of New Hampshire InterOperability Lab","location": "NH","status": 0}'); + return [new LabBlob(jsonObject)]; + } + + static async getLabFlavors(lab_name) { // -> List<FlavorBlob> + const data = await this.handleResponse(this.makeRequest(HTTP.GET, endpoint.FLAVORS, {"lab_name": lab_name})); + let flavors = []; + if (data) { + for (const d of data) { + flavors.push(new FlavorBlob(d)) + } + } else { + apiError("flavors") + } + return flavors; + // let jsonObject = JSON.parse('{"flavor_id": "aaa-bbb-ccc", "name": "HPE Gen 9", "description": "placeholder", "interfaces": ["ens1", "ens2", "ens3"]}') + // return [new FlavorBlob(jsonObject)]; + } + + static async getImagesForFlavor(flavor_id) { + let full_endpoint = endpoint.FLAVORS + flavor_id + '/[username]/' + endpoint.IMAGES; + const data = await this.handleResponse(this.makeRequest(HTTP.GET, full_endpoint, {})); + let images = [] + + if (data) { + for (const d of data) { + images.push(new ImageBlob(d)); + } + } else { + apiError("images") + } + + return images; + // let jsonObject = JSON.parse('{"image_id": "111-222-333", "name": "Arch Linux"}') + // let jsonObject2 = JSON.parse('{"image_id": "444-555-666", "name": "Oracle Linux"}') + // return [new ImageBlob(jsonObject), new ImageBlob(jsonObject2)]; + } + + /** Doesn't need to be passed a username because django will pull this from the request */ + static async getTemplatesForUser() { // -> List<TemplateBlob> + const data = await this.handleResponse(this.makeRequest(HTTP.GET, endpoint.TEMPLATES, {})) + let templates = [] + + if (data) + for (const d of data) { + templates.push(new TemplateBlob(d)) + } else { + apiError("templates") + } + return templates; + // let jsonObject = JSON.parse('{"id": "12345", "owner":"jchoquette", "lab_name":"UNH_IOL","pod_name":"test pod","pod_desc":"for e2e testing","public":false,"host_list":[{"hostname":"test-node","flavor":"1ca6169c-a857-43c6-80b7-09b608c0daec","image":"3fc3833e-7b8b-4748-ab44-eacec8d14f8b","cifile":[],"bondgroups":[{"connections":[{"tagged":true,"connects_to":"public"}],"ifaces":[{"name":"eno49","speed":{"value":10000,"unit":"BitsPerSecond"},"cardtype":"Unknown"}]}]}],"networks":[{"name":"public","public":true}]}') + // let jsonObject2 = JSON.parse('{"id":6789,"owner":"jchoquette","lab_name":"UNH_IOL","pod_name":"Other Host","pod_desc":"Default Template","public":false,"host_list":[{"cifile":["some ci data goes here"],"hostname":"node","flavor":"aaa-bbb-ccc","image":"111-222-333", "bondgroups":[{"connections": [{"tagged": false, "connects_to": "private"}], "ifaces": [{"name": "ens2"}]}]}],"networks":[{"name": "private", "public": false}]}'); + + return [new TemplateBlob(jsonObject)]; + } + + static async saveDesignWorkflow(templateBlob) { // -> bool + templateBlob.owner = user; + return await this.handleResponse(this.makeRequest(HTTP.PUT, endpoint.SAVE_DESIGN_WORKFLOW)) + } + + static async saveBookingWorkflow(bookingBlob) { // -> bool + bookingBlob.owner = user; + return await this.handleResponse(this.makeRequest(HTTP.PUT, endpoint.SAVE_BOOKING_WORKFLOW, bookingBlob)); + } + + static async makeTemplate(templateBlob) { // -> UUID or null + templateBlob.owner = user; + console.log(JSON.stringify(templateBlob)) + return await this.handleResponse(this.makeRequest(HTTP.POST, endpoint.MAKE_TEMPLATE, templateBlob)); + } + + static async deleteTemplate(template_id) { // -> UUID or null + return await this.handleResponse(this.makeRequest(HTTP.DELETE, endpoint.DELETE_TEMPLATE + "/" + template_id, {})); + } + + /** PUT to the dashboard with the bookingBlob. Dashboard will fill in lab and owner, make the django model, then hit liblaas, then come back and fill in the agg_id */ + static async makeBooking(bookingBlob) { + return await this.handleResponse(this.createDashboardBooking(bookingBlob)); + } + + /** Wraps a call in a try / catch, processes the result, and returns the response or null if it failed */ + static async handleResponse(promise) { + try { + let x = await promise; + return x; + } catch(e) { + console.log(e) + return null; + } + } + + /** Uses PUT instead of POST to tell the dashboard that we want to create a dashboard booking instead of a liblaas request */ + static createDashboardBooking(bookingBlob) { + const token = document.getElementsByName('csrfmiddlewaretoken')[0].value + return new Promise((resolve, reject) => { // -> HttpResponse + $.ajax( + { + crossDomain: false, + method: "PUT", + contentType: "application/json; charset=utf-8", + dataType : 'json', + headers: { + 'X-CSRFToken': token + }, + data: JSON.stringify( + bookingBlob), + timeout: 10000, + success: (response) => { + resolve(response); + }, + error: (response) => { + reject(response); + } + } + ) + }) + } +} + + +/** Controller class that handles button inputs to navigate through the workflow and generate HTML dynamically + * Treat this as an abstract class and extend it in the appropriate workflow module. +*/ +class Workflow { + constructor(sections_list) { + this.sections = []; // List of strings + this.step = 0; // Current step of the workflow + this.sections = sections_list; + } + + /** Advances the workflow by one step and scrolls to that section + * Disables the previous button if the step becomes 0 after executing + * Enables the next button if the step is less than sections.length after executing + */ + goPrev() { + + if (workflow.step <= 0) { + return; + } + + this.step--; + + document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'}); + + if (this.step == 0) { + document.getElementById('prev').setAttribute('disabled', ''); + } else if (this.step == this.sections.length - 2) { + document.getElementById('next').removeAttribute('disabled'); + } + } + + goNext() { + if (this.step >= this.sections.length - 1 ) { + return; + } + + this.step++; + document.getElementById(this.sections[this.step]).scrollIntoView({behavior: 'smooth'}); + + if (this.step == this.sections.length - 1) { + document.getElementById('next').setAttribute('disabled', ''); + } else if (this.step == 1) { + document.getElementById('prev').removeAttribute('disabled'); + } + } + + goTo(step_number) { + while (step_number > this.step) { + this.goNext(); + } + + while (step_number < this.step) { + this.goPrev(); + } + } + +} + +function apiError(info) { + alert("Unable to fetch " + info +". Please try again later or contact support.") + } diff --git a/src/templates/base/account/booking_list.html b/src/templates/base/account/booking_list.html index f9234bc..2af3915 100644 --- a/src/templates/base/account/booking_list.html +++ b/src/templates/base/account/booking_list.html @@ -6,15 +6,13 @@ <div class="col-12 col-md-6 col-lg-4 col-xl-3 mb-3"> <div class="card h-100"> <div class="card-header"> - <h3>Booking {{booking.id}}</h3> + <h3>{{booking.purpose}} ({{booking.id}})</h3> </div> <ul class="list-group list-group-flush h-100"> - <li class="list-group-item">id: {{booking.id}}</li> - <li class="list-group-item">lab: {{booking.lab}}</li> - <li class="list-group-item">resource: {{booking.resource.template.name}}</li> - <li class="list-group-item">start: {{booking.start}}</li> - <li class="list-group-item">end: {{booking.end}}</li> - <li class="list-group-item">purpose: {{booking.purpose}}</li> + <li class="list-group-item">Lab: {{booking.lab}}</li> + <li class="list-group-item">Project: {{booking.project}}</li> + <li class="list-group-item">Start: {{booking.start}}</li> + <li class="list-group-item">End: {{booking.end}}</li> </ul> <div class="card-footer d-flex"> <a class="btn btn-primary ml-auto mr-2" href="/booking/detail/{{booking.id}}/">Details</a> @@ -41,15 +39,14 @@ <div class="col-12 col-md-6 col-lg-4 col-xl-3 mb-3"> <div class="card h-100"> <div class="card-header"> - <h3>Booking {{booking.id}}</h3> + <h3>{{booking.purpose}} ({{booking.id}})</h3> </div> <ul class="list-group list-group-flush h-100"> - <li class="list-group-item">id: {{booking.id}}</li> - <li class="list-group-item">lab: {{booking.lab}}</li> - <li class="list-group-item">resource: {{booking.resource.template.name}}</li> - <li class="list-group-item">start: {{booking.start}}</li> - <li class="list-group-item">end: {{booking.end}}</li> - <li class="list-group-item">purpose: {{booking.purpose}}</li> + <li class="list-group-item">Owner: {{booking.owner}}</li> + <li class="list-group-item">Lab: {{booking.lab}}</li> + <li class="list-group-item">Project: {{booking.project}}</li> + <li class="list-group-item">Start: {{booking.start}}</li> + <li class="list-group-item">End: {{booking.end}}</li> </ul> <div class="card-footer d-flex"> <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a> @@ -72,17 +69,15 @@ <div class="col-12 col-md-6 col-lg-4 col-xl-3 mb-3"> <div class="card h-100"> <div class="card-header"> - <h3>Booking {{booking.id}}</h3> + <h3>{{booking.purpose}} ({{booking.id}})</h3> </div> <ul class="list-group list-group-flush h-100"> - <li class="list-group-item">id: {{booking.id}}</li> - <li class="list-group-item">lab: {{booking.lab}}</li> - <li class="list-group-item">resource: {{booking.resource.template.name}}</li> - <li class="list-group-item">start: {{booking.start}}</li> - <li class="list-group-item">end: {{booking.end}}</li> - <li class="list-group-item">purpose: {{booking.purpose}}</li> - <li class="list-group-item">owner: {{booking.owner.userprofile.email_addr}}</li> - </ul> + <li class="list-group-item">Owner: {{booking.owner}}</li> + <li class="list-group-item">Lab: {{booking.lab}}</li> + <li class="list-group-item">Project: {{booking.project}}</li> + <li class="list-group-item">Start: {{booking.start}}</li> + <li class="list-group-item">End: {{booking.end}}</li> + <div class="card-footer d-flex"> <a class="btn btn-primary ml-auto" href="/booking/detail/{{booking.id}}/">Details</a> </div> @@ -101,7 +96,7 @@ current_booking_id = booking_id; } - function submit_cancel_form() { + async function submit_cancel_form() { var ajaxForm = $("#booking_cancel_form"); var formData = ajaxForm.serialize(); req = new XMLHttpRequest(); @@ -110,6 +105,8 @@ req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.onerror = function() { alert("problem submitting form"); } req.send(formData); + await new Promise(r => setTimeout(r, 500)); // Quickest solution I could come up with to give liblaas time to mark it as deleted until we do an api rework + location.reload(); } </script> diff --git a/src/templates/base/account/resource_list.html b/src/templates/base/account/resource_list.html index 33ccaff..c16fd07 100644 --- a/src/templates/base/account/resource_list.html +++ b/src/templates/base/account/resource_list.html @@ -1,21 +1,26 @@ {% extends "base.html" %} +{% block extrahead %} +<script src="/static/js/workflows/workflow.js"></script> +{% endblock %} {% block content %} <div class="row"> -{% for resource in resources %} +{% for resource in templates %} <div class="col-12 col-md-6 col-lg-4 col-xl-3 mb-3"> <div class="card h-100"> <div class="card-header"> - <h3>Resource {{resource.id}}</h3> + <h3>{{resource.pod_name}}</h3> </div> <ul class="list-group list-group-flush h-100"> - <li class="list-group-item">id: {{resource.id}}</li> - <li class="list-group-item">name: {{resource.name}}</li> - <li class="list-group-item">description: {{resource.description}}</li> + <li class="list-group-item">Description: {{resource.pod_desc}}</li> + <li class="list-group-item">Lab: {{resource.lab_name}}</li> + <li class="list-group-item">Hosts: {% for h in resource.host_list %}{{h.hostname}}{% if not forloop.last %}, {% endif %}{% endfor %}</li> + <li class="list-group-item">Networks: {% for n in resource.networks %}{{n.name}}{% if not forloop.last %}, {% endif %}{% endfor %}</li> + </ul> <div class="card-footer"> <button class="btn btn-danger w-100" - onclick='delete_resource({{resource.id}});' + onclick='delete_resource("{{resource.id}}");' data-toggle="modal" data-target="#resModal" >Delete</button> @@ -29,67 +34,19 @@ {% endfor %} </div> <script> - var active_resources = {{active_resources|safe|default:"{}"}} + const user = "{{user}}" var current_resource_id = -1; function delete_resource(resource_id) { document.getElementById("confirm_delete_button").removeAttribute("disabled"); - var warning = document.createTextNode("Are You Sure?"); - var warning_subtext = document.createTextNode("This cannot be undone"); - if(active_resources[resource_id]){ - var warning = document.createTextNode("This resource is being used or is scheduled to be used. It cannot be deleted."); - var warning_subtext = document.createTextNode("If your booking just ended, you may need to give us a few minutes to clean it up before this can be removed."); - - document.getElementById("confirm_delete_button").disabled = true; - } - else { - warning_text = "Are You Sure?"; - warning = document.createTextNode(warning_text); - } - current_resource_id = resource_id; - set_modal_text(warning, warning_subtext); } - function set_modal_text(title, text) { - var clear = function(node) { - while(node.lastChild) { - node.removeChild(node.lastChild); - } + async function submit_delete_form() { + if(LibLaaSAPI.deleteTemplate(current_resource_id)) { + location.reload(); + } else { + alert('Unable to delete template.'); } - var warning_title = document.getElementById("config_warning"); - var warning_text = document.getElementById("warning_subtext"); - - clear(warning_title); - clear(warning_text); - - warning_title.appendChild(title); - warning_text.appendChild(text); - } - - function list_configs(configs) { - var list = document.getElementById("config_list"); - for(var i=0; i<configs.length; i++){ - var str = configs[i].name; - var list_item = document.createElement("LI"); - list_item.appendChild(document.createTextNode(str)); - list.appendChild(list_item); - } - } - - function submit_delete_form() { - var ajaxForm = $("#res_delete_form"); - var formData = ajaxForm.serialize(); - req = new XMLHttpRequest(); - var url = "delete/" + current_resource_id; - req.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - location.reload(); - } - }; - req.open("POST", url, true); - req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - req.onerror = function() { alert("problem submitting form"); } - req.send(formData); } </script> <div class="modal fade" id="resModal" tabindex="-1" role="dialog" aria-hidden="true"> diff --git a/src/templates/base/base.html b/src/templates/base/base.html index 351bd9a..cd4269c 100644 --- a/src/templates/base/base.html +++ b/src/templates/base/base.html @@ -6,7 +6,6 @@ <!-- Custom CSS --> <link href="{% static "css/base.css" %}" rel="stylesheet"> -<script src="/static/js/dashboard.js"></script> {% endblock %} {% block basecontent %} @@ -101,7 +100,7 @@ <!-- Page Content --> <div class="container-fluid d-flex flex-grow-1 px-0 align-items-start flex-column"> <div class="row flex-grow-1 w-100 mx-0 align-content-start flex-lg-grow-1"> - <div class="col-12 col-lg-auto px-0 border-right border-left bg-light" role="navigation"> + <div class="col-12 col-lg-auto px-0 border-right border-left bg-light z-3" role="navigation"> <nav class="navbar navbar-expand-lg border-bottom p-0 w-100 sidebar"> <div class="collapse navbar-collapse" id="sidebar"> <div class="list-group list-group-flush w-100 "> @@ -114,63 +113,45 @@ Create <i class="fas fa-angle-down rotate"></i> </a> <div class="collapse" id="createList"> - <a href="/booking/quick/" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - Express Booking - </a> - <a href="#" onclick="create_workflow(0)" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> + <a href="{% url 'workflow:book_a_pod' %}" onclick="" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> Book a Pod </a> - <a href="#" onclick="create_workflow(1)" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> + <a href="{% url 'workflow:design_a_pod' %}" onclick="" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> Design a Pod </a> - <a href="#" onclick="create_workflow(2)" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - Create a Snapshot - </a> - <a href="#" onclick="create_workflow(3)" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - Configure Anuket - </a> </div> {% endblock dropDown %} - <a href="{% url 'resource:hosts' %}" class="list-group-item list-group-item-action nav-bg"> - Hosts - </a> - {% if user.is_authenticated %} - <a href="{% url 'account:users' %}" class="list-group-item list-group-item-action nav-bg"> - User List - </a> - {% endif %} - <a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action nav-bg"> - Booking List + <a class="list-group-item list-group-item-action nav-bg" data-toggle="collapse" + href="#accountList" role="button"> + Account <i class="fas fa-angle-down rotate"></i> </a> - <a href="{% url 'booking:stats' %}" class="list-group-item list-group-item-action nav-bg"> - Booking Statistics + <div class="collapse" id="accountList"> + <a href="{% url 'account:my-resources' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> + My Resources </a> - <a class="list-group-item list-group-item-action nav-bg" data-toggle="collapse" - href="#accountList" role="button"> - Account <i class="fas fa-angle-down rotate"></i> + <a href="{% url 'account:my-bookings' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> + My Bookings </a> - <div class="collapse" id="accountList"> - <a href="{% url 'account:my-resources' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - My Resources - </a> - <a href="{% url 'account:my-bookings' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - My Bookings - </a> - <a href="{% url 'account:my-images' %}" class="list-group-item list-group-item-action list-group-item-secondary dropDown-bg"> - My Snapshots - </a> - </div> - <a href="{% url 'dashboard:all_labs' %}" class="list-group-item list-group-item-action nav-bg"> - Lab Info + </div> + <a class="list-group-item list-group-item-action nav-bg" data-toggle="collapse" + href="#labInfo" role="button"> + Lab Info <i class="fas fa-angle-down rotate"></i> + </a> + <div class="collapse" id="labInfo"> + <a href="" class="list-group-item list-group-item-action nav-bg"> + Hosts </a> - <a href="{% url 'notifier:messages' %}" class="list-group-item list-group-item-action nav-bg"> - Inbox + <a href="{% url 'booking:list' %}" class="list-group-item list-group-item-action nav-bg"> + Booking List </a> </div> + <a href="{% url 'dashboard:all_labs' %}" class="list-group-item list-group-item-action nav-bg"> + About Us + </a> </div> </nav> </div> - <div class="col overflow-auto flex-grow-1 d-flex flex-column h-100"> + <div class="col flex-grow-1 d-flex flex-column h-100 overflow-control"> {% if title %} <div class="row flex-shrink-1"> <div class="col-lg-12"> @@ -188,4 +169,4 @@ </div> </div> </div> -{% endblock basecontent %} +{% endblock basecontent %}
\ No newline at end of file diff --git a/src/templates/base/booking/booking_detail.html b/src/templates/base/booking/booking_detail.html index 70958f6..e4687ad 100644 --- a/src/templates/base/booking/booking_detail.html +++ b/src/templates/base/booking/booking_detail.html @@ -19,23 +19,29 @@ code { <div class="card mb-3"> <div class="card-header d-flex"> <h4 class="d-inline">Overview</h4> - <button data-toggle="collapse" data-target="#panel_overview" class="btn btn-outline-secondary ml-auto">Expand</button> + <!-- <button data-toggle="collapse" data-target="#panel_overview" class="btn btn-outline-secondary ml-auto">Expand</button> --> </div> <div class="collapse show" id="panel_overview"> <table class="table m-0"> <tr> - <td>Username</td> - <td>{{ posix_username }}</td> + <td>Owner</td> + <td>{{ booking.owner }}</td> </tr> <tr> - <td>Purpose</td> - <td>{{ booking.purpose }}</td> + <td>Collaborators</td> + <td> + {{ collab_string}} + </td> </tr> <tr> <td>Project</td> <td>{{ booking.project }}</td> </tr> <tr> + <td>Purpose</td> + <td>{{ booking.purpose }}</td> + </tr> + <tr> <td>Start Time</td> <td>{{ booking.start }}</td> </tr> @@ -44,185 +50,12 @@ code { <td>{{ booking.end }}</td> </tr> <tr> - <td>Pod Definition</td> - <td>{{ booking.resource.template }}</td> - </tr> - <tr> <td>Lab Deployed At</td> <td>{{ booking.lab }}</td> </tr> </table> </div> </div> - <div class="card my-3"> - <div class="card-header d-flex"> - <h4 class="d-inline">Pod</h4> - <button data-toggle="collapse" data-target="#pod_panel" class="btn btn-outline-secondary ml-auto">Expand</button> - </div> - <div class="collapse show" id="pod_panel"> - <div class="card-body"> - <h4>{{host.bundle.template.copy_of.name}}</h4> - {% for host in booking.resource.get_resources %} - <h4>{{host.config.name}}</h4> - <div class="overflow-auto"> - <table class="table m-0"> - <tr> - <td>Hostname:</td> - <td>{{host.config.name}}</td> - </tr> - <tr> - <td>Machine:</td> - <td>{{host.name}}</td> - </tr> - <tr> - <td>Role:</td> - <td>{{host.template.opnfvRole}}</td> - </tr> - <tr> - <td>Is Headnode:</td> - <td>{{host.config.is_head_node}}</td> - <tr> - <td>Image:</td> - <td id="host_image_{{host.id}}"> - {{host.config.image}} - <button - class="btn btn-primary ml-4" - data-toggle="modal" - data-target="#imageModal" - onclick="set_image_dropdown('{{host.profile.name}}', {{host.id}});" - >Change/Reset</button></td> - </tr> - <tr> - <td>RAM:</td> - <td>{{host.profile.ramprofile.first.amount}}G, - {{host.profile.ramprofile.first.channels}} channels</td> - </tr> - <tr> - <td>CPU:</td> - <td> - <table class="table m-0"> - <tr> - <td>Arch:</td> - <td>{{host.profile.cpuprofile.first.architecture}}</td> - </tr> - <tr> - <td>Cores:</td> - <td>{{host.profile.cpuprofile.first.cores}}</td> - </tr> - <tr> - <td>Sockets:</td> - <td>{{host.profile.cpuprofile.first.cpus}}</td> - </tr> - </table> - </td> - </tr> - <tr> - <td>DISK:</td> - <td> - <table class="table m-0"> - <tr> - <td>Size:</td> - <td>{{host.profile.storageprofile.first.size}} GiB</td> - </tr> - <tr> - <td>Type:</td> - <td>{{host.profile.storageprofile.first.media_type}}</td> - </tr> - <tr> - <td>Mount Point:</td> - <td>{{host.profile.storageprofile.first.name}}</td> - </tr> - </table> - </td> - </tr> - <tr> - <td>Interfaces:</td> - <td> - <table class="table m-0"> - {% for intprof in host.profile.interfaceprofile.all %} - <tr> - <td> - <table class="table table-sm table-borderless m-0"> - <tr> - <td>Name:</td> - <td>{{intprof.name}}</td> - </tr> - <tr> - <td>Speed:</td> - <td>{{intprof.speed}}</td> - </tr> - </table> - </td> - </tr> - {% endfor %} - </table> - </td> - </tr> - </table> - </div> - {% endfor %} - </div> - </div> - </div> - <div class="card my-3"> - <div class="card-header d-flex"> - <h4 class="d-inline">Diagnostic Information</h4> - <button data-toggle="collapse" data-target="#diagnostics_panel" class="btn btn-outline-secondary ml-auto">Expand</button> - </div> - <div class="collapse" id="diagnostics_panel"> - <div class="card-body"> - <table class="table m-0"> - <tr> - <th>Job ID: </th> - <td>{{booking.job.id}}</td> - </tr> - <tr> - <th>CI Files</th> - </tr> - {% for host in booking.resource.get_resources %} - <tr> - <td> - <table class="table m-0"> - <tr> - <th>Host:</th> - <td>{{host.name}}</td> - </tr> - <tr> - <th>Configs:</th> - </tr> - {% for ci_file in host.config.cloud_init_files.all %} - <tr> - <td>{{ci_file.id}}</td> - <td> - <div class="modal fade" id="ci_file_modal_{{ci_file.id}}" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog modal-xl" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title d-inline float-left">Cloud Config Content</h4> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="card-body"> - <pre class="prettyprint lang-yaml m-0 border-0 text-break pre-wrap"> -{{ci_file.text}} - </pre> - </div> - </div> - </div> - </div> - <button class="btn btn-primary" data-toggle="modal" data-target="#ci_file_modal_{{ci_file.id}}">Show File Content</button> - </td> - </tr> - {% endfor %} - </table> - </td> - </tr> - {% endfor %} - </table> - </div> - </div> - </div> </div> <div class="col"> <div class="card mb-3"> @@ -230,144 +63,43 @@ code { <h4 class="d-inline">Deployment Progress</h4> <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> + <!-- <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"> <table class="table m-0"> <tr> <th></th> + <th>Resource</th> <th>Status</th> - <th>Lab Response</th> - <th>Type</th> </tr> - {% for task in booking.job.get_tasklist %} + {% for host in statuses %} <tr> <td> - {% if task.status < 100 %} - <div class="rounded-circle bg-secondary square-20"></div> - {% elif task.status < 200 %} - <div class="spinner-border text-primary square-20"></div> + <!-- Success, + Reimage, + InProgress, + Failure, + Import, --> + {% if host.status is 'Success' %} + <div class="rounded-circle bg-success square-20"></div> + {% elif host.status is 'InProgress' %} + <div class="spinner-border text-primary square-20"></div> {% else %} - <div class="rounded-circle bg-success square-20"></div> + <div class="rounded-circle bg-secondary square-20"></div> {% endif %} </td> <td> - {% if task.status < 100 %} - PENDING - {% elif task.status < 200 %} - IN PROGRESS - {% else %} - DONE - {% endif %} - </td> - <td> - {% if task.message %} - {% if task.type_str == "Access Task" and user_id != task.config.user.id %} - Message from Lab: <pre>--secret--</pre> - {% else %} - Message from Lab: <pre class="text-break pre-wrap">{{ task.message }}</pre> - {% endif %} - {% else %} - No response provided (yet) - {% endif %} + {{ host.hostname }} </td> <td> - {{ task.type_str }} + {{ host.status }} </td> </tr> {% endfor %} </table> </div> </div> - <div class="card my-3"> - <div class="card-header d-flex"> - <h4 class="d-inline">PDF</h4> - <button data-toggle="collapse" data-target="#pdf_panel" class="btn btn-outline-secondary ml-auto">Expand</button> - </div> - <div class="collapse show" id="pdf_panel"> - <div class="card-body"> - <pre class="prettyprint lang-yaml m-0 border-0 text-break pre-wrap"> -{{pdf}} - </pre> - </div> - </div> - </div> </div> </div> - -<div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> - <div class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title d-inline float-left" id="exampleModalLabel">Host Image</h4> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="modal-body"> - <form id="image_host_form"> - {% csrf_token %} - <select class="form-control" id="image_select" name="image_id"> - </select> - <input id="host_id_input" type="hidden" name="host_id"> - </form> - </div> - <div class="modal-footer d-flex flex-column"> - <div> - <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> - <button type="button" class="btn btn-danger" data-toggle="collapse" data-target="#modal_warning" aria-expanded="false">Reset Host</button> - </div> - <div class="border-top collapse mt-3 py-2 text-center w-100" id="modal_warning"> - <h3>Are You Sure?</h3> - <p>This will wipe the disk and reimage the host</p> - <button class="btn btn-outline-secondary" data-dismiss="modal">Nevermind</button> - <button class="btn btn-danger" data-dismiss="modal" onclick="submit_image_form();">I'm Sure</button> - </div> - </div> - </div> - </div> -</div> - -<script> - var image_mapping = {{image_mapping|safe}}; - var current_host_id = 0; - function set_image_dropdown(profile_name, host_id) { - document.getElementById("host_id_input").value = host_id; - current_host_id = host_id; - var dropdown = document.getElementById("image_select"); - var length = dropdown.length; - //clear dropdown - for(i=length-1; i>=0; i--){ - dropdown.options.remove(i); - } - var images = image_mapping[profile_name]; - var image_length = images.length; - for(i=0; i<image_length; i++){ - var opt = document.createElement("OPTION"); - opt.value = images[i].value; - opt.appendChild(document.createTextNode(images[i].name)); - dropdown.options.add(opt); - } - - document.getElementById("modal_warning").classList.add("collapse"); - } - - function submit_image_form() { - var ajaxForm = $("#image_host_form"); - var formData = ajaxForm.serialize(); - req = new XMLHttpRequest(); - req.open("POST", "/booking/modify/{{booking.id}}/image/", true); - req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); - req.onerror = function() { alert("problem submitting form"); } - req.onreadystatechange = function() { - if(req.readyState === 4) { - node = document.getElementById("host_image_" + current_host_id); - text = document.createTextNode(req.responseText); - node.replaceChild(text, node.firstChild); - } - } - req.send(formData); - } -</script> {% endblock content %} diff --git a/src/templates/base/dashboard/genericselect.html b/src/templates/base/dashboard/genericselect.html deleted file mode 100644 index 863d33f..0000000 --- a/src/templates/base/dashboard/genericselect.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "workflow/viewport-element.html" %} - -{% load bootstrap4 %} - -{% block content %} - -<div id="select_form_div" class="h-100 border d-flex flex-column p-4"> - <h3 id="create_section">Create a Resource - <button class="btn btn-primary {% if disabled %} disabled {% endif %}" - {% if not disabled %}onclick="add_workflow({{addable_type_num}})" - {% endif %}>Here - </button> - </h3> - <div class="border-top"></div> - <h3 id="select_header_section">Or select from the list below:</h3> - <div id="select_section" class="d-flex flex-column"> - <form id="step_form" method="post" action="" class="form d-flex flex-column"> - {% csrf_token %} - {{ form|default:"<p>no form loaded</p>" }} - </form> - </div> -</div> - -<script> - {% if disabled %} - disable(); - {% endif %} -</script> - -{% endblock content %} diff --git a/src/templates/base/dashboard/landing.html b/src/templates/base/dashboard/landing.html index 8d6a8f7..fea4deb 100644 --- a/src/templates/base/dashboard/landing.html +++ b/src/templates/base/dashboard/landing.html @@ -56,40 +56,15 @@ {% else %} {% block btnGrp %} <p>To get started, book a server below:</p> - <a class="btn btn-primary btn-lg d-flex flex-column justify-content-center align-content-center border p-4 btnAnuket" href="/booking/quick/" > - Book a Resource + <a class="btn btn-primary btn-lg d-flex flex-column justify-content-center align-content-center border p-4 btnAnuket" href="{% url 'workflow:book_a_pod' %}" > + Book a Pod + </a> + <a class="btn btn-primary btn-lg d-flex flex-column justify-content-center align-content-center border p-4 btnAnuket" href="{% url 'workflow:design_a_pod' %}" > + Design a Pod </a> - <p class="mt-4">PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options - below: - </p> - <div class="btn-group-vertical w-100"> - <button class="btn btn-primary btnAnuket" onclick="create_workflow(0)">Book a Pod</button> - <button class="btn btn-primary btnAnuket" onclick="create_workflow(1)">Design a Pod</button> - </div> {% endblock btnGrp %} {% endif %} </div> - - <!-- Returning users --> - {% if not request.user.is_anonymous %} - {% block returningUsers %} - <div class="col-12 col-lg-6 offset-lg-6 mb-4 mt-lg-4"> - <h2 class="ht-4 border-bottom">Returning Users</h2> - <p>If you're a returning user, some of the following options may be of interest:</p> - <div class="btn-group-vertical w-100"> - <button class="btn btn-primary btnAnuket" onclick="create_workflow(3)">Snapshot a Host</button> - <a class="btn btn-primary btnAnuket" href="{% url 'account:my-bookings' %}"> - My Bookings - </a> - {% if manager == True %} - <button class="btn btn-primary" onclick="continue_workflow()"> - Resume Workflow - </button> - {% endif %} - </div> - </div> - {% endblock returningUsers %} - {% endif %} </div> <div class="hidden_form d-none" id="form_div"> diff --git a/src/templates/base/snapshot_workflow/steps/meta.html b/src/templates/base/snapshot_workflow/steps/meta.html deleted file mode 100644 index 88136d2..0000000 --- a/src/templates/base/snapshot_workflow/steps/meta.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} -{% bootstrap_form_errors form type='non_fields' %} -<div class="p-4"> - <form id="step_form" method="POST" class="form"> - {% csrf_token %} - <div class="form-group"> - {% bootstrap_field form.name %} - {% bootstrap_field form.description %} - </div> - </form> -</div> -{% endblock content %} diff --git a/src/templates/base/snapshot_workflow/steps/select_host.html b/src/templates/base/snapshot_workflow/steps/select_host.html deleted file mode 100644 index 4243145..0000000 --- a/src/templates/base/snapshot_workflow/steps/select_host.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -{% bootstrap_form_errors form type='non_fields' %} -<form id="step_form" method="POST" class="form"> -{% csrf_token %} -<input type="hidden" id="hidden_json_input", name="host"/> -</form> -<div class="container-fluid"> - <div class="row" id="host_select_container"> - </div> -</div> -<script> -var selected_host = null; -var initial = {{chosen|safe|default:'null'}}; - -function select(obj){ - var booking_id = $(obj).attr("booking"); - var host_name = $(obj).attr("hostname"); - var input = document.getElementById("hidden_json_input"); - input.value = JSON.stringify({"booking": booking_id, "name": host_name}); - // clear out and highlist host - if(selected_host != null){ - selected_host.classList.remove("active"); - } - selected_host = document.getElementById("booking_" + booking_id + "_host_" + host_name); - selected_host.classList.add("active"); -} - -function draw_bookings(){ - var booking_hosts = {{booking_hosts|safe}}; - var bookings = []; - var container = document.getElementById("host_select_container"); - for(var booking_id in booking_hosts){ - // Create a column with a card - var column = $("<div/>", { - class: "col-12 col-md-6 col-lg-3 col-xl-2 my-2" - }).appendTo(container); - var booking = $("<div/>", { - class: "card" - }).appendTo(column); - var heading = $("<div/>", { - class: "card-header" - }).text(`Booking ${booking_id}`).appendTo(booking); - var body = $("<ul/>", { - class: "list-group list-group-flush" - }).appendTo(booking); - var footer = $("<div/>", { - text: "Hosts:", - class: "card-footer d-flex flex-column" - }).appendTo(booking); - - // Append information to the card body - $(`<li class="list-group-item">Start: ${booking_hosts[booking_id].start}</li>`).appendTo(body); - $(`<li class="list-group-item">End: ${booking_hosts[booking_id].end}</li>`).appendTo(body); - $(`<li class="list-group-item">Purpose: ${booking_hosts[booking_id].purpose}</li>`).appendTo(body); - - // Append hosts to footer - var hosts = booking_hosts[booking_id].hosts; - for (const host of hosts) { - $("<button/>", { - class: "btn btn-outline-primary w-100 mt-1 hostbtn", - id: `booking_${booking_id}_host_${host.name}`, - text: host.name, - booking: booking_id, - hostname: host.name, - click: function() { - select(this); - } - }).appendTo(footer); - } - } -} -draw_bookings(); -if(initial){ - select(initial.booking_id, initial.hostname); -} -</script> -{% endblock content %} diff --git a/src/templates/base/workflow/book_a_pod.html b/src/templates/base/workflow/book_a_pod.html new file mode 100644 index 0000000..7053bfd --- /dev/null +++ b/src/templates/base/workflow/book_a_pod.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load bootstrap4 %} +{% block extrahead %} +<script src="/static/js/workflows/common-models.js"></script> +<script src="/static/js/workflows/workflow.js"></script> +<script src="/static/js/workflows/book-a-pod.js"></script> +{% endblock %} +{% block content %} +<body> +<div class="workflow-container"> + <div id="prev" class="row w-100 m-0"> + <button class="btn btn-workflow-nav stretched-link m-0 p-0 mt-3" onclick="workflow.goPrev()" id="workflow-prev"> + <div class="arrow arrow-up"></div> + </button> + </div> + + <div id="next" class="row w-100 m-0"> + <button class="btn btn-workflow-nav stretched-link m-0 p-0 mb-3" onclick="workflow.goNext()" id="workflow-next"> + <div class="arrow arrow-down"></div> + </button> + </div> + + <div class="scroll-container w-100 h-100 p-0"> + <div class="scroll-area pt-5 mx-5" id="select_template"> + <h1 class="mt-4"><u>Book a Pod</u></h1> + <h2 class="mt-4 mb-3">Select Host Or Template:</h2> + <div class="card-deck align-items-center"> + + <div class="col-12" id="template_list"> + + <div class="my-5" id="select_template_tab_content"> + <ul id="default_templates_list" class="p-0 m-0 row"> + </ul> + </div> + + </div> + </div> + </div> + + <div class="scroll-area pt-5 mx-5" id="cloud_init"> + <h2 class="mt-4 mb-3">Global Cloud Init Override</h2> + <div class="d-flex align-items-center"> + <textarea name="ci-textarea" id="ci-textarea" rows="15" class="w-50"></textarea> + </div> + </div> + + <div class="scroll-area pt-5 mx-5" id="booking_details"> + <h2 class="mt-4 mb-3">Booking Details</h2> + <div class="form-group mb-0"> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <input id="input_project" class="form-control form-control-lg border border-dark p-5" type="text" placeholder="Project"> + </div> + </div> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <input id="input_purpose" class="form-control form-control-lg border border-dark p-5" type="text" placeholder = "Purpose"> + </div> + </div> + <div class="row align-items-center my-4"> + <span id="booking_details_error" class="text-danger col-xl-6 col-md-8 col-11"></span> + </div> + <div class="row align-items-center my-4"> + <span id="booking_details_day_counter" class="col-md-1 col-2 p-0">Days: 1</span> + <input id="input_length" type="range" min="1" max="21" value="1" class="form-control form-control-lg col-xl-5 col-9 p-0" placeholder="Length" oninput="workflow.onchangeDays()"> + </div> + </div> + </div> + + <div class="scroll-area pt-5 mx-5" id="add_collabs"> + <h2 class="mt-4 mb-3">Add Collaborators:</h2> + <div class="row"> + <div class="col-xl-6 col-md-8 col-11 p-0 border border-dark"> + {% bootstrap_field form.users label="Collaborators" %} + </div> + </div> + </div> + + <div class="scroll-area pt-5 mx-5" id="booking_summary"> + <h2 class="mt-4 mb-3">Booking Summary</h2> + <div class="row align-items-center"> + <div class="card col-xl-6 col-md-8 col-11 border-0"> + <ul class="list-group"> + <li class="list-group-item">Booking Details + <ul id="booking_summary_booking_details"> + </ul> + </li> + <li class="list-group-item">Collaborators + <ul id="booking_summary_collaborators"> + </ul> + </li> + <li class="list-group-item">Hosts + <ul id="booking_summary_hosts"> + </ul> + </li> + </ul> + </div> + </div> + <div class="row align-items-center mt-5"> + <button class="btn btn-danger cancel-book-button p-0 mr-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickCancel()">Cancel</button> + <button class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button> + </div> + </div> + + </div> +</div> + +<div class="hidden_form d-none"> + <form id="token"> + {% csrf_token %} + </form> +</div> +</body> + +<script> + const user = "{{user}}" + const workflow = new BookingWorkflow(); + workflow.startWorkflow(); + </script> +{% endblock %} diff --git a/src/templates/base/workflow/confirm.html b/src/templates/base/workflow/confirm.html deleted file mode 100644 index bc8e4e3..0000000 --- a/src/templates/base/workflow/confirm.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends "workflow/viewport-element.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<div class="text-center"> - <h3>Confirm Session</h3> -</div> -<div class="container"> - <div class="row justify-content-center"> - <div class="col-auto"> - <pre>{{confirmation_info|escape}}</pre> - </div> - </div> - <div class="row"> - <div class="col"> - <div id="form_div" class="text-center p-4"> - <form id="step_form" action="/workflow/manager/" method="post"> - {% csrf_token %} - <div class="d-none"> - {{form|default:"<p> No Form Loaded</p>"}} - </div> - </form> - <div class="cform_buttons mx-auto"> - <button id="confirm_button" class="btn btn-success" onclick="formconfirm()">Confirm</button> - <button id="cancel_button" class="btn btn-danger" onclick="formcancel()">Cancel</button> - </div> - <div class="d-none"> - <form id="manager_delete_form" action="/workflow/finish/" method="post"> - {% csrf_token %} - </form> - </div> - </div> - </div> - </div> -</div> -<script> - var select = document.getElementById("id_confirm"); - - function formconfirm() - { - select.value = "True"; - submitStepForm(); - pop_workflow(); - } - function formcancel() - { - pop_workflow(); - } -</script> -{% block element_messages %} - -{% endblock element_messages %} -{% endblock content %} diff --git a/src/templates/base/workflow/design_a_pod.html b/src/templates/base/workflow/design_a_pod.html new file mode 100644 index 0000000..5d48273 --- /dev/null +++ b/src/templates/base/workflow/design_a_pod.html @@ -0,0 +1,214 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load bootstrap4 %} +{% block extrahead %} +<script src="/static/js/workflows/common-models.js"></script> +<script src="/static/js/workflows/workflow.js"></script> +<script src="/static/js/workflows/design-a-pod.js"></script> +{% endblock %} +{% block content %} +{% if dashboard == 'laas' %} + +<!-- Main Content --> +<body> + + <div class="workflow-container"> + <div id="prev" class="row w-100 m-0"> + <button class="btn btn-workflow-nav stretched-link m-0 p-0 mt-3" onclick="workflow.goPrev()" id="workflow-prev"> + <div class="arrow arrow-up"></div> + </button> + </div> + + <div id="next" class="row w-100 m-0"> + <button class="btn btn-workflow-nav stretched-link m-0 p-0 mb-3" onclick="workflow.goNext()" id="workflow-next"> + <div class="arrow arrow-down"></div> + </button> + </div> + + <div class="scroll-container w-100 h-100 p-0"> + + <!-- Select Lab --> + <div class="scroll-area pt-5 mx-5" id="select_lab"> + <!-- Ideally the 'Design a Pod' header would be anchored to the top of the page below the arrow --> + <h1 class="mt-4"><u>Design a Pod</u></h1> + <h2 class="mt-4 mb-3">Select a Lab:</h2> + <div class="row card-deck" id="lab_cards"> + </div> + </div> + + <!-- Add Resources --> + <div class="scroll-area pt-5 mx-5" id="add_resources"> + <h2 class="mt-4 mb-3">Add Resources:</h2> + <div class="row card-deck align-items-center" id="host_cards"> + <div class="col-xl-3 col-md-6 col-12" id="add_resource_plus_card"> + <div class="card align-items-center border-0"> + <span class="" id="resource-count">0 / 8</span> + <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddResource()">+</button> + </div> + </div> + </div> + </div> + + <!-- Add Networks --> + <div class="scroll-area pt-5 mx-5" id="add_networks"> + <h2 class="mt-4 mb-3">Add Networks:</h2> + <div class="row card-deck align-items-center" id="network_card_deck"> + <div class="col-xl-3 col-md-6 col-12" id="add_network_plus_card"> + <div class="card align-items-center border-0"> + <button class="btn btn-success add-button p-0" onclick="workflow.onclickAddNetwork()">+</button> + </div> + </div> + </div> + </div> + + + <!-- Configure Connections--> + <div class="scroll-area pt-5 mx-5" id="configure_connections"> + <h2 class="mt-4 mb-3">Configure Connections:</h2> + <div class="row card-deck align-items-center" id="connection_cards"> + </div> + </div> + + <!-- Pod Details--> + <div class="scroll-area pt-5 mx-5" id="pod_details"> + <h2 class="mt-4 mb-3">Pod Details</h2> + <div class="form-group"> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <input id="pod-name-input" class="form-control form-control-lg border border-dark p-3" type="text" placeholder="Pod Name"> + </div> + </div> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <textarea id="pod-desc-input" class="form-control form-control-lg border border-dark pt-3 pl-3" rows="5" placeholder="Pod Description"></textarea> + </div> + </div> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <div class="custom-control custom-switch"> + <input type="checkbox" class="custom-control-input" id="pod-public-input"> + <label class="custom-control-label" for="pod-public-input">Make pod template public?</label> + </div> + </div> + </div> + <div class="row align-items-center my-4"> + <div class="col-xl-6 col-md-8 col-11"> + <span id="pod_details_error" class="text-danger"></span> + </div> + </div> + </div> + </div> + + + <!-- Pod Summary--> + <div class="scroll-area pt-5 mx-5" id="pod_summary"> + <h2 class="mt-4 mb-3">Pod Summary:</h2> + <div class="row align-items-center"> + <div class="col-xl-6 col-md-8 col-11"> + <div class="card border-0"> + <ul class="list-group"> + <li class="list-group-item">Pod Details + <ul id="pod_summary_pod_details"> + </ul> + </li> + <li class="list-group-item">Resources + <ul id="pod_summary_hosts"> + </ul> + </li> + </ul> + </div> + </div> + </div> + <div class="row align-items-center mt-5"> + <div class="col-xl-2 col-md-3 col-5"><button class="btn btn-danger cancel-book-button p-0 w-100" onclick="workflow.onclickDiscardTemplate()">Discard</button></div> + <div class="col-xl-2 col-md-3 col-5"><button class="btn btn-success cancel-book-button p-0 w-100" onclick = "workflow.onclickSubmitTemplate()">Create</button></div> + </div> + </div> + + + <!-- End of workflow container and scroll container --> + </div> + </div> + + + + <!-- Modals --> + + <!-- Add Host Modal --> + <div class="modal fade" id="resource_modal" tabindex="-1"> + <div class="modal-dialog modal-xl"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Add Resource</h5> + <button class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button> + </div> + <div class="modal-body" id="add_resource_modal_body"> + <h2>Resource</h2> + <div id="template-cards" class="row align-items-center justify-content-start"> + </div> + + <div id="template-config-section"> + <ul class="nav nav-tabs" role="tablist" id="add_resource_tablist"> + <!-- add a tab per host in template --> + </ul> + <!-- tabs --> + <div id="resource_config_section"> + <h2>Image</h2> + <div id="image-cards" class="row justify-content-start align-items-center"> + </div> + <div class="form-group"> + <h2>Hostname</h2> + <input type="text" class="form-control" id="hostname-input" placeholder="Enter Hostname"> + <h2>Cloud Init</h2> + <div class="d-flex justify-content-center align-items-center"> + <textarea name="ci-textarea" id="ci-textarea" rows="5" class="w-100"></textarea> + </div> + </div> + </div> + </div> + <p id="add-host-error-msg" class="text-danger"></p> + </div> + <div class="modal-footer"> + <button class="btn btn-danger" data-dismiss="modal">Cancel</button> + <button class="btn btn-success" onclick="workflow.onclickSubmitHostConfig()">Submit</button> + </div> + </div> + </div> + </div> + +<!-- Configure Connections modal --> +<div class="modal fade" id="connection_modal" tabindex="-1"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Configure Connections</h5> + <button class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button> + </div> + <div class="modal-body text-center"> + <ul class="nav nav-tabs" role="tablist" id="configure-connections-tablist"> + </ul> + <table id="connections_widget" class="table table-bordered"> + </table> + </div> + <div class="modal-footer"> + <button class="btn btn-success" data-dismiss="modal" id="connection-modal-submit" onclick="workflow.onclickSubmitConnectionConfig()">Confirm</button> + </div> + </div> + </div> +</div> + +<div class="hidden_form d-none"> + <form id="token"> + {% csrf_token %} + </form> +</div> + +</body> + +<script> + const user = "{{user}}" + const workflow = new DesignWorkflow(); + workflow.startWorkflow(); +</script> +{% endif %} +{% endblock %} diff --git a/src/templates/base/workflow/no_workflow.html b/src/templates/base/workflow/no_workflow.html deleted file mode 100644 index 0ac6549..0000000 --- a/src/templates/base/workflow/no_workflow.html +++ /dev/null @@ -1,3 +0,0 @@ -<script> - top.window.location.href='/'; -</script> diff --git a/src/templates/base/workflow/viewport-base.html b/src/templates/base/workflow/viewport-base.html deleted file mode 100644 index 88229ca..0000000 --- a/src/templates/base/workflow/viewport-base.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "base.html" %} -{% load staticfiles %} - -{% load bootstrap4 %} - -{% block content %} - -<!-- Pagination --> -<div class="row mt-3"> - <div class="col"> - <nav> - <ul class="pagination d-flex flex-row" id="topPagination"> - <li class="page-item flex-shrink-1 page-control" id="workflow-nav-back"> - <a class="page-link" href="#" id="gob" onclick="submit_and_go('prev')"> - <i class="fas fa-backward"></i> Back - </a> - </li> - <li class="page-item flex-grow-1"> - <a class="page-link disabled" href="#"> - <i class="far"></i> - </a> - </li> - <li class="page-item flex-shrink-1 page-control" id="workflow-nav-next"> - <a class="page-link text-right" href="#" id="gof" onclick="submit_and_go('next')"> - Next <i class="fas fa-forward"></i> - </a> - </li> - </ul> - </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"> - <div id="iframe_header" class="row view-header"> - <div class="col-lg-12"> - <h1 class="d-inline-block" id="view_title"></h1> - <span class="description text-muted" id="view_desc"></span> - <p id="view_message"></p> - </div> - </div> - </div> - <div class="col-auto align-self-center d-flex"> - <button id="cancel_btn" class="btn btn-danger ml-auto" onclick="pop_workflow()">Cancel</button> - </div> -</div> -<div class="row d-flex flex-column flex-grow-1 mt-3"> - <div class="col flex-grow-1"> - <div id="formContainer" class="h-100 w-100"></div> - </div> -</div> -{% csrf_token %} -<script type="text/javascript"> - function submit_and_go(to) { - submitStepForm(to); - } - - $(document).ready(function(){ - $.ajax({ - url: "/workflow/manager/", - dataType: "json", - success: update_page - }); - }); - - // global variable required for mxgraph to load its css and images - mxBasePath = '{% static "node_modules/mxgraph/javascript/src" %}'; -</script> -<!-- lazy load scripts --> -<script type="text/javascript" src="{% static "node_modules/mxgraph/javascript/mxClient.js" %}" ></script> -<!-- end lazy load scripts --> -<div class="d-none" id="workflow_pop_form_div"> - <form id="workflow_pop_form" action="/workflow/finish/" method="post"> - {% csrf_token %} - </form> -</div> -{% endblock content %} diff --git a/src/templates/base/workflow/viewport-element.html b/src/templates/base/workflow/viewport-element.html deleted file mode 100644 index db4da54..0000000 --- a/src/templates/base/workflow/viewport-element.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load bootstrap4 %} -{% load staticfiles %} - -{% block basecontent %} - - {% block content %} - {% endblock content %} - - <div class="messages"> - {% block element_messages %} - {% bootstrap_messages %} - {% endblock %} - </div> -{% endblock basecontent %} - -{% block extrajs %} -{% endblock extrajs %} diff --git a/src/workflow/README b/src/workflow/README index fb4b949..565d1c2 100644 --- a/src/workflow/README +++ b/src/workflow/README @@ -1,31 +1 @@ -This app creates "workflows", which are long and complex interactions from the user. -Workflows are composed of multiple steps. At each step the user inputs some information. -The content of one step may impact following steps. - -The WorkflowStep object is the abstract type for all the workflow steps. -Important attributes and methods: - -template - the django template to use when rendering this step -valid - the status code from WorkflowStepStatus - -get_context() - returns a dictionary that is used when rendering this step's template - You should always call super's get_context and add / overwrite any data into that - dictionary - -post(data, user) - this method is called when the step is POST'd to. - data is from the request object, suitable for a Form's constructor - - -Repository -Each step has a reference to a shared repository (self.repo). -The repo is a key-value store that allows the steps to share data - -Steps render based on the current state of the repo. For example, a step -may get information about each host the user said they want and ask for additional -input for each machine. -Because the steps render based on what is in the repo, a user can easily go back to -a previous step and change some data. This data will change in the repo and -affect later steps accordingly. - -Everything stored in the repo is temporary. After a workflow has been completed, the repo -is translated into Django models and saved to the database. +TODO: Document how new workflows work diff --git a/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py deleted file mode 100644 index ef89804..0000000 --- a/src/workflow/booking_workflow.py +++ /dev/null @@ -1,182 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. -# Copyright (c) 2020 Sawyer Bergeron, 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.utils import timezone - -from datetime import timedelta - -from booking.models import Booking -from workflow.models import WorkflowStep, AbstractSelectOrCreate -from workflow.forms import ResourceSelectorForm, BookingMetaForm, OPNFVSelectForm -from resource_inventory.models import OPNFVConfig, ResourceTemplate -from django.db.models import Q - - -""" -subclassing notes: - subclasses have to define the following class attributes: - self.repo_key: main output of step, where the selected/created single selector - result is placed at the end - self.confirm_key: -""" - - -class Abstract_Resource_Select(AbstractSelectOrCreate): - form = ResourceSelectorForm - template = 'dashboard/genericselect.html' - title = "Select Resource" - description = "Select a resource template to use for your deployment" - short_title = "pod select" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.select_repo_key = self.repo.SELECTED_RESOURCE_TEMPLATE - self.confirm_key = self.workflow_type - - def alert_bundle_missing(self): - self.set_invalid("Please select a valid resource template") - - def get_form_queryset(self): - user = self.repo_get(self.repo.SESSION_USER) - return ResourceTemplate.objects.filter((Q(owner=user) | Q(public=True))) - - def get_page_context(self): - return { - 'select_type': 'resource', - 'select_type_title': 'Resource template', - 'addable_type_num': 1 - } - - def put_confirm_info(self, bundle): - confirm_dict = self.repo_get(self.repo.CONFIRMATION) - if self.confirm_key not in confirm_dict: - confirm_dict[self.confirm_key] = {} - confirm_dict[self.confirm_key]["Resource Template"] = bundle.name - self.repo_put(self.repo.CONFIRMATION, confirm_dict) - - -class Booking_Resource_Select(Abstract_Resource_Select): - workflow_type = "booking" - - -class OPNFV_EnablePicker(object): - pass - - -class OPNFV_Select(AbstractSelectOrCreate, OPNFV_EnablePicker): - title = "Choose an OPNFV Config" - description = "Choose or create a description of how you want to deploy OPNFV" - short_title = "opnfv config" - form = OPNFVSelectForm - enabled = False - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.select_repo_key = self.repo.SELECTED_OPNFV_CONFIG - self.confirm_key = "booking" - - def alert_bundle_missing(self): - self.set_invalid("Please select a valid OPNFV config") - - def get_form_queryset(self): - cb = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) - qs = OPNFVConfig.objects.filter(bundle=cb) - return qs - - def put_confirm_info(self, config): - confirm_dict = self.repo_get(self.repo.CONFIRMATION) - if self.confirm_key not in confirm_dict: - confirm_dict[self.confirm_key] = {} - confirm_dict[self.confirm_key]["OPNFV Configuration"] = config.name - self.repo_put(self.repo.CONFIRMATION, confirm_dict) - - def get_page_context(self): - return { - 'select_type': 'opnfv', - 'select_type_title': 'OPNFV Config', - 'addable_type_num': 4 - } - - -class Booking_Meta(WorkflowStep): - template = 'booking/steps/booking_meta.html' - title = "Extra Info" - description = "Tell us how long you want your booking, what it is for, and who else should have access to it" - short_title = "booking info" - - def get_context(self): - context = super(Booking_Meta, self).get_context() - initial = {} - default = [] - try: - models = self.repo_get(self.repo.BOOKING_MODELS, {}) - booking = models.get("booking") - if booking: - initial['purpose'] = booking.purpose - initial['project'] = booking.project - initial['length'] = (booking.end - booking.start).days - info = self.repo_get(self.repo.BOOKING_INFO_FILE, False) - if info: - initial['info_file'] = info - users = models.get("collaborators", []) - for user in users: - default.append(user.userprofile) - except Exception: - pass - - owner = self.repo_get(self.repo.SESSION_USER) - - context['form'] = BookingMetaForm(initial=initial, user_initial=default, owner=owner) - return context - - def post(self, post_data, user): - form = BookingMetaForm(data=post_data, owner=user) - - forms = self.repo_get(self.repo.BOOKING_FORMS, {}) - - forms["meta_form"] = form - self.repo_put(self.repo.BOOKING_FORMS, forms) - - if form.is_valid(): - models = self.repo_get(self.repo.BOOKING_MODELS, {}) - if "booking" not in models: - models['booking'] = Booking() - models['collaborators'] = [] - confirm = self.repo_get(self.repo.CONFIRMATION) - if "booking" not in confirm: - confirm['booking'] = {} - - models['booking'].start = timezone.now() - models['booking'].end = timezone.now() + timedelta(days=int(form.cleaned_data['length'])) - models['booking'].purpose = form.cleaned_data['purpose'] - models['booking'].project = form.cleaned_data['project'] - for key in ['length', 'project', 'purpose']: - confirm['booking'][key] = form.cleaned_data[key] - - if form.cleaned_data["deploy_opnfv"]: - self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=True) - else: - self.repo_get(self.repo.SESSION_MANAGER).set_step_statuses(OPNFV_EnablePicker, desired_enabled=False) - - userprofile_list = form.cleaned_data['users'] - confirm['booking']['collaborators'] = [] - for userprofile in userprofile_list: - models['collaborators'].append(userprofile.user) - confirm['booking']['collaborators'].append(userprofile.user.username) - - info_file = form.cleaned_data.get("info_file", False) - if info_file: - self.repo_put(self.repo.BOOKING_INFO_FILE, info_file) - - self.repo_put(self.repo.BOOKING_MODELS, models) - self.repo_put(self.repo.CONFIRMATION, confirm) - self.set_valid("Step Completed") - else: - self.set_invalid("Please complete the fields highlighted in red to continue") diff --git a/src/workflow/forms.py b/src/workflow/forms.py index 62abad6..da36e83 100644 --- a/src/workflow/forms.py +++ b/src/workflow/forms.py @@ -20,12 +20,7 @@ import urllib from account.models import Lab from account.models import UserProfile -from resource_inventory.models import ( - OPNFVRole, - Installer, - Scenario -) -from resource_inventory.resource_manager import ResourceManager + from booking.lib import get_user_items, get_user_field_opts @@ -206,50 +201,6 @@ class OPNFVSelectForm(SearchableSelectAbstractForm): return items -class ResourceSelectorForm(SearchableSelectAbstractForm): - def generate_items(self, queryset): - items = {} - - for bundle in queryset: - items[bundle.id] = { - 'expanded_name': bundle.name, - 'small_name': bundle.owner.username, - 'string': bundle.description, - 'id': bundle.id - } - - return items - - -class BookingMetaForm(forms.Form): - # Django Form class for Book a Pod - length = forms.IntegerField( - widget=NumberInput( - attrs={ - "type": "range", - 'min': "1", - "max": "21", - "value": "1" - } - ) - ) - purpose = forms.CharField(max_length=1000) - project = forms.CharField(max_length=400) - info_file = forms.CharField(max_length=1000, required=False) - deploy_opnfv = forms.BooleanField(required=False) - - def __init__(self, *args, user_initial=[], owner=None, **kwargs): - super(BookingMetaForm, self).__init__(**kwargs) - - self.fields['users'] = SearchableSelectMultipleField( - queryset=UserProfile.objects.select_related('user').exclude(user=owner), - initial=user_initial, - items=get_user_items(exclude=owner), - required=False, - **get_user_field_opts() - ) - - class MultipleSelectFilterWidget(forms.Widget): def __init__(self, *args, display_objects=None, filter_items=None, neighbors=None, **kwargs): super(MultipleSelectFilterWidget, self).__init__(*args, **kwargs) @@ -284,206 +235,16 @@ class MultipleSelectFilterField(forms.Field): except json.decoder.JSONDecodeError: pass raise ValidationError("content is not valid JSON") + +class BookingMetaForm(forms.Form): + def __init__(self, *args, user_initial=[], owner=None, **kwargs): + super(BookingMetaForm, self).__init__(**kwargs) -class FormUtils: - @staticmethod - def getLabData(multiple_hosts=False, user=None): - """ - Get all labs and thier host profiles, returns a serialized version the form can understand. - - Could be rewritten with a related query to make it faster - """ - # javascript truthy variables - true = 1 - false = 0 - if multiple_hosts: - multiple_hosts = true - else: - multiple_hosts = false - labs = {} - resources = {} - items = {} - neighbors = {} - for lab in Lab.objects.all(): - lab_node = { - 'id': "lab_" + str(lab.lab_user.id), - 'model_id': lab.lab_user.id, - 'name': lab.name, - 'description': lab.description, - 'selected': false, - 'selectable': true, - 'follow': multiple_hosts, - 'multiple': false, - 'class': 'lab', - 'available_resources': json.dumps(lab.get_available_resources()) - } - - items[lab_node['id']] = lab_node - neighbors[lab_node['id']] = [] - labs[lab_node['id']] = lab_node - - for template in ResourceManager.getInstance().getAvailableResourceTemplates(lab, user): - resource_node = { - 'form': {"name": "host_name", "type": "text", "placeholder": "hostname"}, - 'id': "resource_" + str(template.id), - 'model_id': template.id, - 'name': template.name, - 'description': template.description, - 'selected': false, - 'selectable': true, - 'follow': false, - 'multiple': multiple_hosts, - 'class': 'resource', - 'required_resources': json.dumps(template.get_required_resources()) - } - - if multiple_hosts: - resource_node['values'] = [] # place to store multiple values - - items[resource_node['id']] = resource_node - neighbors[lab_node['id']].append(resource_node['id']) - - if resource_node['id'] not in neighbors: - neighbors[resource_node['id']] = [] - - neighbors[resource_node['id']].append(lab_node['id']) - resources[resource_node['id']] = resource_node - - display_objects = [("lab", labs.values()), ("resource", resources.values())] - - context = { - 'display_objects': display_objects, - 'neighbors': neighbors, - 'filter_items': items - } - - return context - - -class HardwareDefinitionForm(forms.Form): - - def __init__(self, user, *args, **kwargs): - super(HardwareDefinitionForm, self).__init__(*args, **kwargs) - attrs = FormUtils.getLabData(multiple_hosts=True, user=user) - self.fields['filter_field'] = MultipleSelectFilterField( - widget=MultipleSelectFilterWidget(**attrs) - ) - - -class PodDefinitionForm(forms.Form): - - fields = ["xml"] - xml = forms.CharField() - - -class ResourceMetaForm(forms.Form): - - bundle_name = forms.CharField(label="POD Name") - bundle_description = forms.CharField(label="POD Description", widget=forms.Textarea, max_length=1000) - - -class GenericHostMetaForm(forms.Form): - - host_profile = forms.CharField(label="Host Type", disabled=True, required=False) - host_name = forms.CharField(label="Host Name") - - -class NetworkDefinitionForm(forms.Form): - def __init__(self, *args, **kwargs): - super(NetworkDefinitionForm, self).__init__(**kwargs) - - -class NetworkConfigurationForm(forms.Form): - def __init__(self, *args, **kwargs): - super(NetworkConfigurationForm).__init__(**kwargs) - - -class HostSoftwareDefinitionForm(forms.Form): - # Django Form class for Design a Pod - host_name = forms.CharField( - max_length=200, - disabled=False, - required=True - ) - headnode = forms.BooleanField(required=False, widget=forms.HiddenInput) - - def __init__(self, *args, **kwargs): - imageQS = kwargs.pop("imageQS") - super(HostSoftwareDefinitionForm, self).__init__(*args, **kwargs) - self.fields['image'] = forms.ModelChoiceField(queryset=imageQS) - - -class WorkflowSelectionForm(forms.Form): - fields = ['workflow'] - - empty_permitted = False - - workflow = forms.ChoiceField( - choices=( - (0, 'Booking'), - (1, 'Resource Bundle'), - (2, 'Software Configuration') - ), - label="Choose Workflow", - initial='booking', - required=True - ) - - -class SnapshotHostSelectForm(forms.Form): - host = forms.CharField() - - -class BasicMetaForm(forms.Form): - name = forms.CharField() - description = forms.CharField(widget=forms.Textarea) - - -class ConfirmationForm(forms.Form): - fields = ['confirm'] - - confirm = forms.ChoiceField( - choices=( - (False, "Cancel"), - (True, "Confirm") - ) - ) - - -def validate_step(value): - if value not in ["prev", "next", "current"]: - raise ValidationError(str(value) + " is not allowed") - - -def validate_step_form(value): - try: - urllib.parse.unquote_plus(value) - except Exception: - raise ValidationError("Value is not url encoded data") - - -class ManagerForm(forms.Form): - step = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step]) - step_form = forms.CharField(widget=forms.widgets.HiddenInput, validators=[validate_step_form]) - # other fields? - - -class OPNFVSelectionForm(forms.Form): - installer = forms.ModelChoiceField(queryset=Installer.objects.all(), required=True) - scenario = forms.ModelChoiceField(queryset=Scenario.objects.all(), required=True) - - -class OPNFVNetworkRoleForm(forms.Form): - role = forms.CharField(max_length=200, disabled=True, required=False) - - def __init__(self, *args, config_bundle, **kwargs): - super(OPNFVNetworkRoleForm, self).__init__(*args, **kwargs) - self.fields['network'] = forms.ModelChoiceField( - queryset=config_bundle.bundle.networks.all() - ) - - -class OPNFVHostRoleForm(forms.Form): - host_name = forms.CharField(max_length=200, disabled=True, required=False) - role = forms.ModelChoiceField(queryset=OPNFVRole.objects.all().order_by("name").distinct("name")) + self.fields['users'] = SearchableSelectMultipleField( + queryset=UserProfile.objects.select_related('user').exclude(user=owner), + initial=user_initial, + items=get_user_items(exclude=owner), + required=False, + **get_user_field_opts() + )
\ No newline at end of file diff --git a/src/workflow/models.py b/src/workflow/models.py index e065202..f69ee85 100644 --- a/src/workflow/models.py +++ b/src/workflow/models.py @@ -5,689 +5,4 @@ # 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.loader import get_template -from django.http import HttpResponse -from django.utils import timezone -from django.db import transaction - -import yaml -import requests - -from workflow.forms import ConfirmationForm -from api.models import JobFactory -from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException -from resource_inventory.models import Image, OPNFVConfig, ResourceOPNFVConfig, NetworkRole -from resource_inventory.resource_manager import ResourceManager -from resource_inventory.pdf_templater import PDFTemplater -from notifier.manager import NotificationHandler -from booking.models import Booking - - -class BookingAuthManager(): - """ - Verifies Booking Authorization. - - Class to verify that the user is allowed to book the requested resource - The user must input a url to the INFO.yaml file to prove that they are the ptl of - an approved project if they are booking a multi-node pod. - This class parses the url and checks the logged in user against the info file. - """ - - LFN_PROJECTS = ["opnfv"] # TODO - - def parse_github_url(self, url): - project_leads = [] - try: - parts = url.split("/") - if "http" in parts[0]: # the url include http(s):// - parts = parts[2:] - if parts[-1] != "INFO.yaml": - return None - if parts[0] not in ["github.com", "raw.githubusercontent.com"]: - return None - if parts[1] not in self.LFN_PROJECTS: - return None - # now to download and parse file - if parts[3] == "blob": - parts[3] = "raw" - url = "https://" + "/".join(parts) - info_file = requests.get(url, timeout=15).text - info_parsed = yaml.load(info_file) - ptl = info_parsed.get('project_lead') - if ptl: - project_leads.append(ptl) - sub_ptl = info_parsed.get("subproject_lead") - if sub_ptl: - project_leads.append(sub_ptl) - - except Exception: - pass - - return project_leads - - def parse_gerrit_url(self, url): - project_leads = [] - try: - halfs = url.split("?") - parts = halfs[0].split("/") - args = halfs[1].split(";") - if "http" in parts[0]: # the url include http(s):// - parts = parts[2:] - if "f=INFO.yaml" not in args: - return None - if "gerrit.opnfv.org" not in parts[0]: - return None - try: - i = args.index("a=blob") - args[i] = "a=blob_plain" - except ValueError: - pass - # recreate url - halfs[1] = ";".join(args) - halfs[0] = "/".join(parts) - # now to download and parse file - url = "https://" + "?".join(halfs) - info_file = requests.get(url, timeout=15).text - info_parsed = yaml.load(info_file) - ptl = info_parsed.get('project_lead') - if ptl: - project_leads.append(ptl) - sub_ptl = info_parsed.get("subproject_lead") - if sub_ptl: - project_leads.append(sub_ptl) - - except Exception: - return None - - return project_leads - - def parse_opnfv_git_url(self, url): - project_leads = [] - try: - parts = url.split("/") - if "http" in parts[0]: # the url include http(s):// - parts = parts[2:] - if "INFO.yaml" not in parts[-1]: - return None - if "git.opnfv.org" not in parts[0]: - return None - if parts[-2] == "tree": - parts[-2] = "plain" - # now to download and parse file - url = "https://" + "/".join(parts) - info_file = requests.get(url, timeout=15).text - info_parsed = yaml.load(info_file) - ptl = info_parsed.get('project_lead') - if ptl: - project_leads.append(ptl) - sub_ptl = info_parsed.get("subproject_lead") - if sub_ptl: - project_leads.append(sub_ptl) - - except Exception: - return None - - return project_leads - - def parse_url(self, info_url): - """ - Parse the project URL. - - Gets the INFO.yaml file from the project and returns the PTL info. - """ - if "github" in info_url: - return self.parse_github_url(info_url) - - if "gerrit.opnfv.org" in info_url: - return self.parse_gerrit_url(info_url) - - if "git.opnfv.org" in info_url: - return self.parse_opnfv_git_url(info_url) - - def booking_allowed(self, booking, repo): - """ - Assert the current Booking Policy. - - This is the method that will have to change whenever the booking policy changes in the Infra - group / LFN. This is a nice isolation of that administration crap - currently checks if the booking uses multiple servers. if it does, then the owner must be a PTL, - which is checked using the provided info file - """ - if booking.owner.userprofile.booking_privledge: - return True # admin override for this user - if Booking.objects.filter(owner=booking.owner, end__gt=timezone.now()).count() >= 3: - return False - if len(booking.resource.template.get_required_resources()) < 2: - return True # if they only have one server, we dont care - if repo.BOOKING_INFO_FILE not in repo.el: - return False # INFO file not provided - ptl_info = self.parse_url(repo.el.get(repo.BOOKING_INFO_FILE)) - for ptl in ptl_info: - if ptl['email'] == booking.owner.userprofile.email_addr: - return True - return False - - -class WorkflowStepStatus(object): - """ - Poor man's enum for the status of a workflow step. - - The steps in a workflow are not completed (UNTOUCHED) - or they have been completed correctly (VALID) or they were filled out - incorrectly (INVALID) - """ - - UNTOUCHED = 0 - INVALID = 100 - VALID = 200 - - -class WorkflowStep(object): - template = 'bad_request.html' - title = "Generic Step" - description = "You were led here by mistake" - short_title = "error" - metastep = None - # phasing out metastep: - - valid = WorkflowStepStatus.UNTOUCHED - message = "" - - enabled = True - - def cleanup(self): - raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method") - - def enable(self): - if not self.enabled: - self.enabled = True - - def disable(self): - if self.enabled: - self.cleanup() - self.enabled = False - - def set_invalid(self, message, code=WorkflowStepStatus.INVALID): - self.valid = code - self.message = message - - def set_valid(self, message, code=WorkflowStepStatus.VALID): - self.valid = code - self.message = message - - def to_json(self): - return { - 'title': self.short_title, - 'enabled': self.enabled, - 'valid': self.valid, - 'message': self.message, - } - - def __init__(self, id, repo=None): - self.repo = repo - self.id = id - - def get_context(self): - context = {} - context['step_number'] = self.repo_get('steps') - context['active_step'] = self.repo_get('active_step') - context['render_correct'] = "true" - context['step_title'] = self.title - context['description'] = self.description - return context - - def render(self, request): - return HttpResponse(self.render_to_string(request)) - - def render_to_string(self, request): - template = get_template(self.template) - return template.render(self.get_context(), request) - - def post(self, post_content, user): - raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete post() method") - - def validate(self, request): - pass - - def repo_get(self, key, default=None): - return self.repo.get(key, default, self.id) - - def repo_put(self, key, value): - return self.repo.put(key, value, self.id) - - -""" -subclassing notes: - subclasses have to define the following class attributes: - self.select_repo_key: where the selected "object" or "bundle" is to be placed in the repo - self.form: the form to be used - alert_bundle_missing(): what message to display if a user does not select/selects an invalid object - get_form_queryset(): generate a queryset to be used to filter available items for the field - get_page_context(): return simple context such as page header and other info -""" - - -class AbstractSelectOrCreate(WorkflowStep): - template = 'dashboard/genericselect.html' - title = "Select a Bundle" - short_title = "select" - description = "Generic bundle selector step" - - select_repo_key = None - form = None # subclasses are expected to use a form that is a subclass of SearchableSelectGenericForm - - def alert_bundle_missing(self): # override in subclasses to change message if field isn't filled out - self.set_invalid("Please select a valid bundle") - - def post(self, post_data, user): - form = self.form(post_data, queryset=self.get_form_queryset()) - if form.is_valid(): - bundle = form.get_validated_bundle() - if not bundle: - self.alert_bundle_missing() - return - self.repo_put(self.select_repo_key, bundle) - self.put_confirm_info(bundle) - self.set_valid("Step Completed") - else: - self.alert_bundle_missing() - - def get_context(self): - default = [] - - bundle = self.repo_get(self.select_repo_key, False) - if bundle: - default.append(bundle) - - form = self.form(queryset=self.get_form_queryset(), initial=default) - - context = {'form': form, **self.get_page_context()} - context.update(super().get_context()) - - return context - - def get_page_context(): - return { - 'select_type': 'generic', - 'select_type_title': 'Generic Bundle' - } - - -class Confirmation_Step(WorkflowStep): - template = 'workflow/confirm.html' - title = "Confirm Changes" - description = "Does this all look right?" - - short_title = "confirm" - - def get_context(self): - context = super(Confirmation_Step, self).get_context() - context['form'] = ConfirmationForm() - # Summary of submitted form data shown on the 'confirm' step of the workflow - confirm_details = "\nPod:\n Name: '{name}'\n Description: '{desc}'\nLab: '{lab}'".format( - name=self.repo_get(self.repo.CONFIRMATION)['resource']['name'], - desc=self.repo_get(self.repo.CONFIRMATION)['resource']['description'], - lab=self.repo_get(self.repo.CONFIRMATION)['template']['lab']) - confirm_details += "\nResources:" - for i, device in enumerate(self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['resources']): - confirm_details += "\n " + str(device) + ": " + str(self.repo_get(self.repo.CONFIRMATION)['template']['resources'][i]['profile']) - context['confirmation_info'] = confirm_details - if self.valid == WorkflowStepStatus.VALID: - context["confirm_succeeded"] = "true" - - return context - - def flush_to_db(self): - errors = self.repo.make_models() - if errors: - return errors - - def post(self, post_data, user): - form = ConfirmationForm(post_data) - if form.is_valid(): - data = form.cleaned_data['confirm'] - if data == "True": - errors = self.flush_to_db() - if errors: - self.set_invalid("ERROR OCCURRED: " + errors) - else: - self.set_valid("Confirmed") - - elif data == "False": - self.repo.cancel() - self.set_valid("Canceled") - else: - self.set_invalid("Bad Form Contents") - - else: - self.set_invalid("Bad Form Contents") - - -class Repository(): - - EDIT = "editing" - MODELS = "models" - RESOURCE_SELECT = "resource_select" - CONFIRMATION = "confirmation" - SELECTED_RESOURCE_TEMPLATE = "selected resource template pk" - SELECTED_OPNFV_CONFIG = "selected opnfv deployment config" - RESOURCE_TEMPLATE_MODELS = "generic_resource_template_models" - RESOURCE_TEMPLATE_INFO = "generic_resource_template_info" - BOOKING = "booking" - LAB = "lab" - RCONFIG_LAST_HOSTLIST = "resource_configuration_network_previous_hostlist" - BOOKING_FORMS = "booking_forms" - SWCONF_HOSTS = "swconf_hosts" - BOOKING_MODELS = "booking models" - CONFIG_MODELS = "configuration bundle models" - OPNFV_MODELS = "opnfv configuration models" - SESSION_USER = "session owner user account" - SESSION_MANAGER = "session manager for current session" - VALIDATED_MODEL_GRB = "valid grb config model instance in db" - VALIDATED_MODEL_CONFIG = "valid config model instance in db" - VALIDATED_MODEL_BOOKING = "valid booking model instance in db" - VLANS = "a list of vlans" - SNAPSHOT_MODELS = "the models for snapshotting" - SNAPSHOT_BOOKING_ID = "the booking id for snapshotting" - SNAPSHOT_NAME = "the name of the snapshot" - SNAPSHOT_DESC = "description of the snapshot" - BOOKING_INFO_FILE = "the INFO.yaml file for this user's booking" - - # new keys for migration to using ResourceTemplates: - RESOURCE_TEMPLATE_MODELS = "current working model of resource template" - - # migratory elements of segmented workflow - # each of these is the end result of a different workflow. - HAS_RESULT = "whether or not workflow has a result" - RESULT_KEY = "key for target index that result will be put into in parent" - RESULT = "result object from workflow" - - def get_child_defaults(self): - return_tuples = [] - for key in [self.SELECTED_RESOURCE_TEMPLATE, self.SESSION_USER]: - return_tuples.append((key, self.el.get(key))) - return return_tuples - - def set_defaults(self, defaults): - for key, value in defaults: - self.el[key] = value - - def get(self, key, default, id): - - self.add_get_history(key, id) - return self.el.get(key, default) - - def put(self, key, val, id): - self.add_put_history(key, id) - self.el[key] = val - - def add_get_history(self, key, id): - self.add_history(key, id, self.get_history) - - def add_put_history(self, key, id): - self.add_history(key, id, self.put_history) - - def add_history(self, key, id, history): - if key not in history: - history[key] = [id] - else: - history[key].append(id) - - def cancel(self): - if self.RESOURCE_TEMPLATE_MODELS in self.el: - models = self.el[self.RESOURCE_TEMPLATE_MODELS] - if models['template'].temporary: - models['template'].delete() - # deleting current template should cascade delete all - # necessary related models - - def make_models(self): - if self.SNAPSHOT_MODELS in self.el: - errors = self.make_snapshot() - if errors: - return errors - - # if GRB WF, create it - if self.RESOURCE_TEMPLATE_MODELS in self.el: - errors = self.make_generic_resource_bundle() - if errors: - return errors - else: - self.el[self.HAS_RESULT] = True - self.el[self.RESULT_KEY] = self.SELECTED_RESOURCE_TEMPLATE - return - - if self.OPNFV_MODELS in self.el: - errors = self.make_opnfv_config() - if errors: - return errors - else: - self.el[self.HAS_RESULT] = True - self.el[self.RESULT_KEY] = self.SELECTED_OPNFV_CONFIG - - if self.BOOKING_MODELS in self.el: - errors = self.make_booking() - if errors: - return errors - # create notification - booking = self.el[self.BOOKING_MODELS]['booking'] - NotificationHandler.notify_new_booking(booking) - - def make_snapshot(self): - owner = self.el[self.SESSION_USER] - models = self.el[self.SNAPSHOT_MODELS] - image = models.get('snapshot', Image()) - booking_id = self.el.get(self.SNAPSHOT_BOOKING_ID) - if not booking_id: - return "SNAP, No booking ID provided" - booking = Booking.objects.get(pk=booking_id) - if booking.start > timezone.now() or booking.end < timezone.now(): - return "Booking is not active" - name = self.el.get(self.SNAPSHOT_NAME) - if not name: - return "SNAP, no name provided" - host = models.get('host') - if not host: - return "SNAP, no host provided" - description = self.el.get(self.SNAPSHOT_DESC, "") - image.from_lab = booking.lab - image.name = name - image.description = description - image.public = False - image.lab_id = -1 - image.owner = owner - image.host_type = host.profile - image.save() - try: - current_image = host.config.image - image.os = current_image.os - image.save() - except Exception: - pass - JobFactory.makeSnapshotTask(image, booking, host) - - self.el[self.RESULT] = image - self.el[self.HAS_RESULT] = True - - def make_generic_resource_bundle(self): - owner = self.el[self.SESSION_USER] - if self.RESOURCE_TEMPLATE_MODELS in self.el: - models = self.el[self.RESOURCE_TEMPLATE_MODELS] - models['template'].owner = owner - models['template'].temporary = False - models['template'].save() - self.el[self.RESULT] = models['template'] - self.el[self.HAS_RESULT] = True - return False - - else: - return "GRB no models given. CODE:0x0001" - - def make_software_config_bundle(self): - models = self.el[self.CONFIG_MODELS] - if 'bundle' in models: - bundle = models['bundle'] - bundle.bundle = self.el[self.SELECTED_RESOURCE_TEMPLATE] - try: - bundle.save() - except Exception as e: - return "SWC, saving bundle generated exception: " + str(e) + "CODE:0x0007" - - else: - return "SWC, no bundle in models. CODE:0x0006" - if 'host_configs' in models: - host_configs = models['host_configs'] - for host_config in host_configs: - host_config.template = host_config.template - host_config.profile = host_config.profile - try: - host_config.save() - except Exception as e: - return "SWC, saving host configs generated exception: " + str(e) + "CODE:0x0009" - else: - return "SWC, no host configs in models. CODE:0x0008" - if 'opnfv' in models: - opnfvconfig = models['opnfv'] - opnfvconfig.bundle = opnfvconfig.bundle - if opnfvconfig.scenario not in opnfvconfig.installer.sup_scenarios.all(): - return "SWC, scenario not supported by installer. CODE:0x000d" - try: - opnfvconfig.save() - except Exception as e: - return "SWC, saving opnfv config generated exception: " + str(e) + "CODE:0x000b" - else: - pass - - self.el[self.RESULT] = bundle - return False - - @transaction.atomic # TODO: Rewrite transactions with savepoints at user level for all workflows - def make_booking(self): - models = self.el[self.BOOKING_MODELS] - owner = self.el[self.SESSION_USER] - - if 'booking' in models: - booking = models['booking'] - else: - return "BOOK, no booking model exists. CODE:0x000f" - - selected_grb = None - - if self.SELECTED_RESOURCE_TEMPLATE in self.el: - selected_grb = self.el[self.SELECTED_RESOURCE_TEMPLATE] - else: - return "BOOK, no selected resource. CODE:0x000e" - - if not booking.start: - return "BOOK, booking has no start. CODE:0x0010" - if not booking.end: - return "BOOK, booking has no end. CODE:0x0011" - if booking.end <= booking.start: - return "BOOK, end before/same time as start. CODE:0x0012" - - if 'collaborators' in models: - collaborators = models['collaborators'] - else: - return "BOOK, collaborators not defined. CODE:0x0013" - try: - res_manager = ResourceManager.getInstance() - resource_bundle = res_manager.instantiateTemplate(selected_grb) - except ResourceAvailabilityException as e: - return "BOOK, requested resources are not available. Exception: " + str(e) + " CODE:0x0014" - except ModelValidationException as e: - return "Error encountered when saving bundle. " + str(e) + " CODE: 0x001b" - - booking.resource = resource_bundle - booking.owner = owner - booking.lab = selected_grb.lab - - is_allowed = BookingAuthManager().booking_allowed(booking, self) - if not is_allowed: - return "BOOK, you are not allowed to book the requested resources" - - try: - booking.save() - except Exception as e: - return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0015" - - for collaborator in collaborators: - booking.collaborators.add(collaborator) - - try: - booking.pdf = PDFTemplater.makePDF(booking) - booking.save() - except Exception as e: - return "BOOK, failed to create Pod Desriptor File: " + str(e) - - try: - JobFactory.makeCompleteJob(booking) - except Exception as e: - return "BOOK, serializing for api generated exception: " + str(e) + " CODE:0xFFFF" - - try: - booking.save() - except Exception as e: - return "BOOK, saving booking generated exception: " + str(e) + " CODE:0x0016" - - self.el[self.RESULT] = booking - self.el[self.HAS_RESULT] = True - - def make_opnfv_config(self): - opnfv_models = self.el[self.OPNFV_MODELS] - config_bundle = self.el[self.SELECTED_CONFIG_BUNDLE] - if not config_bundle: - return "No Configuration bundle selected" - info = opnfv_models.get("meta", {}) - name = info.get("name", False) - desc = info.get("description", False) - if not (name and desc): - return "No name or description given" - installer = opnfv_models['installer_chosen'] - if not installer: - return "No OPNFV Installer chosen" - scenario = opnfv_models['scenario_chosen'] - if not scenario: - return "No OPNFV Scenario chosen" - - opnfv_config = OPNFVConfig.objects.create( - bundle=config_bundle, - name=name, - description=desc, - installer=installer, - scenario=scenario - ) - - network_roles = opnfv_models['network_roles'] - for net_role in network_roles: - opnfv_config.networks.add( - NetworkRole.objects.create( - name=net_role['role'], - network=net_role['network'] - ) - ) - - host_roles = opnfv_models['host_roles'] - for host_role in host_roles: - config = config_bundle.hostConfigurations.get( - host__resource__name=host_role['host_name'] - ) - ResourceOPNFVConfig.objects.create( - role=host_role['role'], - host_config=config, - opnfv_config=opnfv_config - ) - - self.el[self.RESULT] = opnfv_config - self.el[self.HAS_RESULT] = True - - def __init__(self): - self.el = {} - self.el[self.CONFIRMATION] = {} - self.el["active_step"] = 0 - self.el[self.HAS_RESULT] = False - self.el[self.RESULT] = None - self.get_history = {} - self.put_history = {} +##############################################################################
\ No newline at end of file diff --git a/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py deleted file mode 100644 index 6ffc91d..0000000 --- a/src/workflow/opnfv_workflow.py +++ /dev/null @@ -1,292 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.forms import formset_factory - -from workflow.models import WorkflowStep, AbstractSelectOrCreate -from resource_inventory.models import ResourceTemplate, OPNFV_SETTINGS -from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm - - -class OPNFV_Resource_Select(AbstractSelectOrCreate): - title = "Select Software Configuration" - description = "Choose the software bundle you wish to use as a base for your OPNFV configuration" - short_title = "software config" - form = SWConfigSelectorForm - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.select_repo_key = self.repo.SELECTED_CONFIG_BUNDLE - - def get_form_queryset(self): - user = self.repo_get(self.repo.SESSION_USER) - qs = ResourceTemplate.objects.filter(owner=user) - return qs - - def put_confirm_info(self, bundle): - confirm_dict = self.repo_get(self.repo.CONFIRMATION) - confirm_dict['software bundle'] = bundle.name - confirm_dict['hardware POD'] = bundle.bundle.name - self.repo_put(self.repo.CONFIRMATION, confirm_dict) - - def get_page_context(self): - return { - 'select_type': 'swconfig', - 'select_type_title': 'Software Config', - 'addable_type_num': 2 - } - - -class Pick_Installer(WorkflowStep): - template = 'config_bundle/steps/pick_installer.html' - title = 'Pick OPNFV Installer' - description = 'Choose which OPNFV installer to use' - short_title = "opnfv installer" - modified_key = "installer_step" - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - installer = models.get("installer_chosen") - scenario = models.get("scenario_chosen") - if not (installer and scenario): - return - confirm['installer'] = installer.name - confirm['scenario'] = scenario.name - self.repo_put(self.repo.CONFIRMATION, confirm) - - def get_context(self): - context = super(Pick_Installer, self).get_context() - - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - initial = { - "installer": models.get("installer_chosen"), - "scenario": models.get("scenario_chosen") - } - - context["form"] = OPNFVSelectionForm(initial=initial) - return context - - def post(self, post_data, user): - form = OPNFVSelectionForm(post_data) - if form.is_valid(): - installer = form.cleaned_data['installer'] - scenario = form.cleaned_data['scenario'] - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - models['installer_chosen'] = installer - models['scenario_chosen'] = scenario - self.repo_put(self.repo.OPNFV_MODELS, models) - self.update_confirmation() - self.set_valid("Step Completed") - else: - self.set_invalid("Please select an Installer and Scenario") - - -class Assign_Network_Roles(WorkflowStep): - template = 'config_bundle/steps/assign_network_roles.html' - title = 'Pick Network Roles' - description = 'Choose what role each network should get' - short_title = "network roles" - modified_key = "net_roles_step" - - """ - to do initial filling, repo should have a "network_roles" array with the following structure for each element: - { - "role": <NetworkRole object ref>, - "network": <Network object ref> - } - """ - def create_netformset(self, roles, config_bundle, data=None): - roles_initial = [] - set_roles = self.repo_get(self.repo.OPNFV_MODELS, {}).get("network_roles") - if set_roles: - roles_initial = set_roles - else: - for role in OPNFV_SETTINGS.NETWORK_ROLES: - roles_initial.append({"role": role}) - - Formset = formset_factory(OPNFVNetworkRoleForm, extra=0) - kwargs = { - "initial": roles_initial, - "form_kwargs": {"config_bundle": config_bundle} - } - formset = None - if data: - formset = Formset(data, **kwargs) - else: - formset = Formset(**kwargs) - return formset - - def get_context(self): - context = super(Assign_Network_Roles, self).get_context() - config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) - if config_bundle is None: - context["unavailable"] = True - return context - - roles = OPNFV_SETTINGS.NETWORK_ROLES - formset = self.create_netformset(roles, config_bundle) - context['formset'] = formset - - return context - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - roles = models.get("network_roles") - if not roles: - return - confirm['network roles'] = {} - for role in roles: - confirm['network roles'][role['role']] = role['network'].name - self.repo_put(self.repo.CONFIRMATION, confirm) - - def post(self, post_data, user): - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - config_bundle = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) - roles = OPNFV_SETTINGS.NETWORK_ROLES - net_role_formset = self.create_netformset(roles, config_bundle, data=post_data) - if net_role_formset.is_valid(): - results = [] - for form in net_role_formset: - results.append({ - "role": form.cleaned_data['role'], - "network": form.cleaned_data['network'] - }) - models['network_roles'] = results - self.set_valid("Completed") - self.repo_put(self.repo.OPNFV_MODELS, models) - self.update_confirmation() - else: - self.set_invalid("Please complete all fields") - - -class Assign_Host_Roles(WorkflowStep): # taken verbatim from Define_Software in sw workflow, merge the two? - template = 'config_bundle/steps/assign_host_roles.html' - title = 'Pick Host Roles' - description = "Choose the role each machine will have in your OPNFV pod" - short_title = "host roles" - modified_key = "host_roles_step" - - def create_host_role_formset(self, hostlist=[], data=None): - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - host_roles = models.get("host_roles", []) - if not host_roles: - for host in hostlist: - initial = {"host_name": host.resource.name} - host_roles.append(initial) - models['host_roles'] = host_roles - self.repo_put(self.repo.OPNFV_MODELS, models) - - HostFormset = formset_factory(OPNFVHostRoleForm, extra=0) - - kwargs = {"initial": host_roles} - formset = None - if data: - formset = HostFormset(data, **kwargs) - else: - formset = HostFormset(**kwargs) - - return formset - - def get_context(self): - context = super(Assign_Host_Roles, self).get_context() - config = self.repo_get(self.repo.SELECTED_CONFIG_BUNDLE) - if config is None: - context['error'] = "Please select a Configuration on the first step" - - formset = self.create_host_role_formset(hostlist=config.bundle.getResources()) - context['formset'] = formset - - return context - - def get_host_role_mapping(self, host_roles, hostname): - for obj in host_roles: - if hostname == obj['host_name']: - return obj - return None - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - roles = models.get("host_roles") - if not roles: - return - confirm['host roles'] = {} - for role in roles: - confirm['host roles'][role['host_name']] = role['role'].name - self.repo_put(self.repo.CONFIRMATION, confirm) - - def post(self, post_data, user): - formset = self.create_host_role_formset(data=post_data) - - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - host_roles = models.get("host_roles", []) - - has_jumphost = False - if formset.is_valid(): - for form in formset: - hostname = form.cleaned_data['host_name'] - role = form.cleaned_data['role'] - mapping = self.get_host_role_mapping(host_roles, hostname) - mapping['role'] = role - if "jumphost" in role.name.lower(): - has_jumphost = True - - models['host_roles'] = host_roles - self.repo_put(self.repo.OPNFV_MODELS, models) - self.update_confirmation() - - if not has_jumphost: - self.set_invalid('Must have at least one "Jumphost" per POD') - else: - self.set_valid("Completed") - else: - self.set_invalid("Please complete all fields") - - -class MetaInfo(WorkflowStep): - template = 'config_bundle/steps/config_software.html' - title = "Other Info" - description = "Give your software config a name, description, and other stuff" - short_title = "config info" - - def get_context(self): - context = super(MetaInfo, self).get_context() - - initial = self.repo_get(self.repo.OPNFV_MODELS, {}).get("meta", {}) - context["form"] = BasicMetaForm(initial=initial) - return context - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - meta = models.get("meta") - if not meta: - return - confirm['name'] = meta['name'] - confirm['description'] = meta['description'] - self.repo_put(self.repo.CONFIRMATION, confirm) - - def post(self, post_data, user): - models = self.repo_get(self.repo.OPNFV_MODELS, {}) - info = models.get("meta", {}) - - form = BasicMetaForm(post_data) - if form.is_valid(): - info['name'] = form.cleaned_data['name'] - info['description'] = form.cleaned_data['description'] - models['meta'] = info - self.repo_put(self.repo.OPNFV_MODELS, models) - self.update_confirmation() - self.set_valid("Complete") - else: - self.set_invalid("Please correct the errors shown below") - self.repo_put(self.repo.OPNFV_MODELS, models) diff --git a/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py deleted file mode 100644 index 4e288b5..0000000 --- a/src/workflow/resource_bundle_workflow.py +++ /dev/null @@ -1,614 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.forms import formset_factory -from django.core.exceptions import ValidationError - -from typing import List - -import re -import json -from xml.dom import minidom -import traceback - -from workflow.models import WorkflowStep -from account.models import Lab -from workflow.forms import ( - HardwareDefinitionForm, - NetworkDefinitionForm, - ResourceMetaForm, - HostSoftwareDefinitionForm, -) -from resource_inventory.models import ( - ResourceTemplate, - ResourceConfiguration, - InterfaceConfiguration, - Network, - NetworkConnection, - Image, -) -from dashboard.exceptions import ( - InvalidVlanConfigurationException, - NetworkExistsException, - ResourceAvailabilityException -) - -import logging -logger = logging.getLogger(__name__) - - -class Define_Hardware(WorkflowStep): - - template = 'resource/steps/define_hardware.html' - title = "Define Hardware" - description = "Choose the type and amount of machines you want" - short_title = "hosts" - - def __init__(self, *args, **kwargs): - self.form = None - super().__init__(*args, **kwargs) - - def get_context(self): - context = super(Define_Hardware, self).get_context() - user = self.repo_get(self.repo.SESSION_USER) - context['form'] = self.form or HardwareDefinitionForm(user) - return context - - def update_models(self, data): - data = data['filter_field'] - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - models['resources'] = [] # This will always clear existing data when this step changes - models['connections'] = [] - models['interfaces'] = {} - if "template" not in models: - template = ResourceTemplate.objects.create(temporary=True) - models['template'] = template - - resource_data = data['resource'] - - new_template = models['template'] - - public_network = Network.objects.create(name="public", bundle=new_template, is_public=True) - - all_networks = {public_network.id: public_network} - - for resource_template_dict in resource_data.values(): - id = resource_template_dict['id'] - old_template = ResourceTemplate.objects.get(id=id) - - # instantiate genericHost and store in repo - for _ in range(0, resource_template_dict['count']): - resource_configs = old_template.resourceConfigurations.all() - for config in resource_configs: - # need to save now for connections to refer to it later - new_config = ResourceConfiguration.objects.create( - profile=config.profile, - image=config.image, - name=config.name, - template=new_template) - - for interface_config in config.interface_configs.all(): - new_interface_config = InterfaceConfiguration.objects.create( - profile=interface_config.profile, - resource_config=new_config) - - for connection in interface_config.connections.all(): - network = None - if connection.network.is_public: - network = public_network - else: - # check if network is known - if connection.network.id not in all_networks: - # create matching one - new_network = Network( - name=connection.network.name + "_" + str(new_config.id), - bundle=new_template, - is_public=False) - new_network.save() - - all_networks[connection.network.id] = new_network - - network = all_networks[connection.network.id] - - new_connection = NetworkConnection( - network=network, - vlan_is_tagged=connection.vlan_is_tagged) - - new_interface_config.save() # can't do later because M2M on next line - new_connection.save() - - new_interface_config.connections.add(new_connection) - - unique_resource_ref = new_config.name + "_" + str(new_config.id) - if unique_resource_ref not in models['interfaces']: - models['interfaces'][unique_resource_ref] = [] - models['interfaces'][unique_resource_ref].append(interface_config) - - models['resources'].append(new_config) - - models['networks'] = all_networks - - # add selected lab to models - for lab_dict in data['lab'].values(): - if lab_dict['selected']: - models['template'].lab = Lab.objects.get(lab_user__id=lab_dict['id']) - models['template'].save() - break # if somehow we get two 'true' labs, we only use one - - # return to repo - self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - if "template" not in confirm: - confirm['template'] = {} - confirm['template']['resources'] = [] - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - if 'template' in models: - for resource in models['template'].getConfigs(): - host_dict = {"name": resource.name, "profile": resource.profile.name} - confirm['template']['resources'].append(host_dict) - if "template" in models: - confirm['template']['lab'] = models['template'].lab.lab_user.username - self.repo_put(self.repo.CONFIRMATION, confirm) - - def post(self, post_data, user): - try: - user = self.repo_get(self.repo.SESSION_USER) - self.form = HardwareDefinitionForm(user, post_data) - if self.form.is_valid(): - self.update_models(self.form.cleaned_data) - self.update_confirmation() - self.set_valid("Step Completed") - else: - self.set_invalid("Please complete the fields highlighted in red to continue") - except Exception as e: - print("Caught exception: " + str(e)) - traceback.print_exc() - self.form = None - self.set_invalid("Please select a lab.") - - -class Define_Software(WorkflowStep): - template = 'config_bundle/steps/define_software.html' - title = "Pick Software" - description = "Choose the opnfv and image of your machines" - short_title = "host config" - - def build_filter_data(self, hosts_data): - """ - Build list of Images to filter out. - - returns a 2D array of images to exclude - based on the ordering of the passed - hosts_data - """ - - filter_data = [] - user = self.repo_get(self.repo.SESSION_USER) - lab = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS)['template'].lab - for i, host_data in enumerate(hosts_data): - host = ResourceConfiguration.objects.get(pk=host_data['host_id']) - wrong_owner = Image.objects.exclude(owner=user).exclude(public=True) - wrong_host = Image.objects.exclude(architecture=host.profile.architecture) - wrong_lab = Image.objects.exclude(from_lab=lab) - excluded_images = wrong_owner | wrong_host | wrong_lab - filter_data.append([]) - for image in excluded_images: - filter_data[i].append(image.pk) - return filter_data - - def create_hostformset(self, hostlist, data=None): - hosts_initial = [] - configs = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}).get("resources") - if configs: - for i in range(len(configs)): - default_name = 'laas-node' - if i > 0: - default_name = default_name + "-" + str(i + 1) - hosts_initial.append({ - 'host_id': configs[i].id, - 'host_name': default_name, - 'headnode': False, - 'image': configs[i].image - }) - else: - for host in hostlist: - hosts_initial.append({ - 'host_id': host.id, - 'host_name': host.name - }) - - HostFormset = formset_factory(HostSoftwareDefinitionForm, extra=0) - filter_data = self.build_filter_data(hosts_initial) - - class SpecialHostFormset(HostFormset): - def get_form_kwargs(self, index): - kwargs = super(SpecialHostFormset, self).get_form_kwargs(index) - if index is not None: - kwargs['imageQS'] = Image.objects.exclude(pk__in=filter_data[index]) - return kwargs - - if data: - return SpecialHostFormset(data, initial=hosts_initial) - return SpecialHostFormset(initial=hosts_initial) - - def get_host_list(self, grb=None): - return self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS).get("resources") - - def get_context(self): - context = super(Define_Software, self).get_context() - - context["formset"] = self.create_hostformset(self.get_host_list()) - - return context - - def post(self, post_data, user): - hosts = self.get_host_list() - formset = self.create_hostformset(hosts, data=post_data) - has_headnode = False - if formset.is_valid(): - for i, form in enumerate(formset): - host = hosts[i] - image = form.cleaned_data['image'] - hostname = form.cleaned_data['host_name'] - headnode = form.cleaned_data['headnode'] - if headnode: - has_headnode = True - host.is_head_node = headnode - host.name = hostname - host.image = image - # RFC921: They must start with a letter, end with a letter or digit and have only letters or digits or hyphen as interior characters - if bool(re.match("^[A-Za-z0-9-]*$", hostname)) is False: - self.set_invalid("Device names must only contain alphanumeric characters and dashes.") - return - if not hostname[0].isalpha() or not hostname[-1].isalnum(): - self.set_invalid("Device names must start with a letter and end with a letter or digit.") - return - for j in range(i): - if j != i and hostname == hosts[j].name: - self.set_invalid("Devices must have unique names. Please try again.") - return - host.save() - - if not has_headnode and len(hosts) > 0: - self.set_invalid("No headnode. Please set a headnode.") - return - - self.set_valid("Completed") - else: - self.set_invalid("Please complete all fields.") - - -class Define_Nets(WorkflowStep): - template = 'resource/steps/pod_definition.html' - title = "Define Networks" - description = "Use the tool below to draw the network topology of your POD" - short_title = "networking" - form = NetworkDefinitionForm - - def get_vlans(self): - vlans = self.repo_get(self.repo.VLANS) - if vlans: - return vlans - # try to grab some vlans from lab - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - if "bundle" not in models: - return None - lab = models['bundle'].lab - if lab is None or lab.vlan_manager is None: - return None - try: - vlans = lab.vlan_manager.get_vlans(count=lab.vlan_manager.block_size) - self.repo_put(self.repo.VLANS, vlans) - return vlans - except Exception: - return None - - def make_mx_network_dict(self, network): - return { - 'id': network.id, - 'name': network.name, - 'public': network.is_public - } - - def make_mx_resource_dict(self, resource_config): - resource_dict = { - 'id': resource_config.id, - 'interfaces': [], - 'value': { - 'name': resource_config.name, - 'id': resource_config.id, - 'description': resource_config.profile.description - } - } - - for interface_config in resource_config.interface_configs.all(): - connections = [] - for connection in interface_config.connections.all(): - connections.append({'tagged': connection.vlan_is_tagged, 'network': connection.network.id}) - - interface_dict = { - "id": interface_config.id, - "name": interface_config.profile.name, - "description": "speed: " + str(interface_config.profile.speed) + "M\ntype: " + interface_config.profile.nic_type, - "connections": connections - } - - resource_dict['interfaces'].append(interface_dict) - - return resource_dict - - def make_mx_host_dict(self, generic_host): - host = { - 'id': generic_host.profile.name, - 'interfaces': [], - 'value': { - "name": generic_host.profile.name, - "description": generic_host.profile.description - } - } - for iface in generic_host.profile.interfaceprofile.all(): - host['interfaces'].append({ - "name": iface.name, - "description": "speed: " + str(iface.speed) + "M\ntype: " + iface.nic_type - }) - return host - - # first step guards this one, so can't get here without at least empty - # models being populated by step one - def get_context(self): - context = super(Define_Nets, self).get_context() - context.update({ - 'form': NetworkDefinitionForm(), - 'debug': settings.DEBUG, - 'resources': {}, - 'networks': {}, - 'vlans': [], - # remove others - 'hosts': [], - 'added_hosts': [], - 'removed_hosts': [] - }) - - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS) # infallible, guarded by prior step - for resource in models['resources']: - d = self.make_mx_resource_dict(resource) - context['resources'][d['id']] = d - - for network in models['networks'].values(): - d = self.make_mx_network_dict(network) - context['networks'][d['id']] = d - - return context - - def post(self, post_data, user): - try: - xmlData = post_data.get("xml") - self.updateModels(xmlData) - # update model with xml - self.set_valid("Networks applied successfully") - except ResourceAvailabilityException: - self.set_invalid("Public network not availble") - except Exception as e: - traceback.print_exc() - self.set_invalid("An error occurred when applying networks: " + str(e)) - - def resetNetworks(self, networks: List[Network]): # potentially just pass template here? - for network in networks: - network.delete() - - def updateModels(self, xmlData): - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - given_hosts = None - interfaces = None - networks = None - try: - given_hosts, interfaces, networks = self.parseXml(xmlData) - except Exception as e: - print("tried to parse Xml, got exception instead:") - print(e) - - existing_rconfig_list = models.get("resources", []) - existing_rconfigs = {} # maps id to host - for rconfig in existing_rconfig_list: - existing_rconfigs["host_" + str(rconfig.id)] = rconfig - - bundle = models.get("template") # hard fail if not in repo - - self.resetNetworks(models['networks'].values()) - models['networks'] = {} - - for net_id, net in networks.items(): - network = Network.objects.create( - name=net['name'], - bundle=bundle, - is_public=net['public']) - - models['networks'][net_id] = network - network.save() - - for hostid, given_host in given_hosts.items(): - for ifaceId in given_host['interfaces']: - iface = interfaces[ifaceId] - - iface_config = InterfaceConfiguration.objects.get(id=iface['config_id']) - if iface_config.resource_config.template.id != bundle.id: - raise ValidationError("User does not own the template they are editing") - - for connection in iface['connections']: - network_id = connection['network'] - net = models['networks'][network_id] - connection = NetworkConnection(vlan_is_tagged=connection['tagged'], network=net) - connection.save() - iface_config.connections.add(connection) - iface_config.save() - self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) - - def decomposeXml(self, xmlString): - """ - Translate XML into useable data. - - This function takes in an xml doc from our front end - and returns dictionaries that map cellIds to the xml - nodes themselves. There is no unpacking of the - xml objects, just grouping and organizing - """ - connections = {} - networks = {} - hosts = {} - interfaces = {} - network_ports = {} - - xmlDom = minidom.parseString(xmlString) - root = xmlDom.documentElement.firstChild - for cell in root.childNodes: - cellId = cell.getAttribute('id') - group = cellId.split("_")[0] - parentGroup = cell.getAttribute("parent").split("_")[0] - # place cell into correct group - - if cell.getAttribute("edge"): - connections[cellId] = cell - - elif "network" in group: - networks[cellId] = cell - - elif "host" in group: - hosts[cellId] = cell - - elif "host" in parentGroup: - interfaces[cellId] = cell - - # make network ports also map to thier network - elif "network" in parentGroup: - network_ports[cellId] = cell.getAttribute("parent") # maps port ID to net ID - - return connections, networks, hosts, interfaces, network_ports - - # serialize and deserialize xml from mxGraph - def parseXml(self, xmlString): - networks = {} # maps net name to network object - hosts = {} # cotains id -> hosts, each containing interfaces, referencing networks - interfaces = {} # maps id -> interface - untagged_ifaces = set() # used to check vlan config - network_names = set() # used to check network names - xml_connections, xml_nets, xml_hosts, xml_ifaces, xml_ports = self.decomposeXml(xmlString) - - # parse Hosts - for cellId, cell in xml_hosts.items(): - cell_json_str = cell.getAttribute("value") - cell_json = json.loads(cell_json_str) - host = {"interfaces": [], "name": cellId, "hostname": cell_json['name']} - hosts[cellId] = host - - # parse networks - for cellId, cell in xml_nets.items(): - escaped_json_str = cell.getAttribute("value") - json_str = escaped_json_str.replace('"', '"') - net_info = json.loads(json_str) - net_name = net_info['name'] - public = net_info['public'] - if net_name in network_names: - raise NetworkExistsException("Non unique network name found") - network = {"name": net_name, "public": public, "id": cellId} - networks[cellId] = network - network_names.add(net_name) - - # parse interfaces - for cellId, cell in xml_ifaces.items(): - parentId = cell.getAttribute('parent') - cell_json_str = cell.getAttribute("value") - cell_json = json.loads(cell_json_str) - iface = {"graph_id": cellId, "connections": [], "config_id": cell_json['id'], "profile_name": cell_json['name']} - hosts[parentId]['interfaces'].append(cellId) - interfaces[cellId] = iface - - # parse connections - for cellId, cell in xml_connections.items(): - escaped_json_str = cell.getAttribute("value") - json_str = escaped_json_str.replace('"', '"') - attributes = json.loads(json_str) - tagged = attributes['tagged'] - interface = None - network = None - src = cell.getAttribute("source") - tgt = cell.getAttribute("target") - if src in interfaces: - interface = interfaces[src] - network = networks[xml_ports[tgt]] - else: - interface = interfaces[tgt] - network = networks[xml_ports[src]] - - if not tagged: - if interface['config_id'] in untagged_ifaces: - raise InvalidVlanConfigurationException("More than one untagged vlan on an interface") - untagged_ifaces.add(interface['config_id']) - - # add connection to interface - interface['connections'].append({"tagged": tagged, "network": network['id']}) - - return hosts, interfaces, networks - - -class Resource_Meta_Info(WorkflowStep): - template = 'resource/steps/meta_info.html' - title = "Extra Info" - description = "Please fill out the rest of the information about your resource" - short_title = "pod info" - - def update_confirmation(self): - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - if "template" not in confirm: - confirm['template'] = {} - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - if "template" in models: - confirm['template']['description'] = models['template'].description - confirm['template']['name'] = models['template'].name - self.repo_put(self.repo.CONFIRMATION, confirm) - - def get_context(self): - context = super(Resource_Meta_Info, self).get_context() - name = "" - desc = "" - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, None) - bundle = models['template'] - if bundle: - name = bundle.name - desc = bundle.description - context['form'] = ResourceMetaForm(initial={"bundle_name": name, "bundle_description": desc}) - return context - - def post(self, post_data, user): - form = ResourceMetaForm(post_data) - if form.is_valid(): - models = self.repo_get(self.repo.RESOURCE_TEMPLATE_MODELS, {}) - name = form.cleaned_data['bundle_name'] - desc = form.cleaned_data['bundle_description'] - bundle = models['template'] # infallible - bundle.name = name - bundle.description = desc - bundle.save() - self.repo_put(self.repo.RESOURCE_TEMPLATE_MODELS, models) - confirm = self.repo_get(self.repo.CONFIRMATION) - if "resource" not in confirm: - confirm['resource'] = {} - confirm_info = confirm['resource'] - confirm_info["name"] = name - tmp = desc - if len(tmp) > 60: - tmp = tmp[:60] + "..." - confirm_info["description"] = tmp - self.repo_put(self.repo.CONFIRMATION, confirm) - self.set_valid("Step Completed") - else: - self.set_invalid("Please complete all fields.") diff --git a/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py deleted file mode 100644 index c0e2052..0000000 --- a/src/workflow/snapshot_workflow.py +++ /dev/null @@ -1,116 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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.utils import timezone -import json - -from booking.models import Booking -from resource_inventory.models import ResourceQuery, Image -from workflow.models import WorkflowStep -from workflow.forms import BasicMetaForm, SnapshotHostSelectForm - - -class Select_Host_Step(WorkflowStep): - template = "snapshot_workflow/steps/select_host.html" - title = "Select Host" - description = "Choose which machine you want to snapshot" - short_title = "host" - - def get_context(self): - context = super(Select_Host_Step, self).get_context() - context['form'] = SnapshotHostSelectForm() - booking_hosts = {} - now = timezone.now() - user = self.repo_get(self.repo.SESSION_USER) - bookings = Booking.objects.filter(start__lt=now, end__gt=now, owner=user) - for booking in bookings: - booking_hosts[booking.id] = {} - booking_hosts[booking.id]['purpose'] = booking.purpose - booking_hosts[booking.id]['start'] = booking.start.strftime("%Y-%m-%d") - booking_hosts[booking.id]['end'] = booking.end.strftime("%Y-%m-%d") - booking_hosts[booking.id]['hosts'] = [] - for genericHost in booking.resource.template.getResources(): - booking_hosts[booking.id]['hosts'].append({"name": genericHost.resource.name}) - - context['booking_hosts'] = booking_hosts - - chosen_host = self.repo_get(self.repo.SNAPSHOT_MODELS, {}).get("host") - if chosen_host: - chosen = {} - chosen['booking_id'] = self.repo_get(self.repo.SNAPSHOT_BOOKING_ID) - chosen['hostname'] = chosen_host.template.resource.name - context['chosen'] = chosen - return context - - def post(self, post_data, user): - host_data = post_data.get("host") - if not host_data: - self.set_invalid("Please select a host") - return - host = json.loads(host_data) - if 'name' not in host or 'booking' not in host: - self.set_invalid("Invalid host selected") - return - name = host['name'] - booking_id = host['booking'] - booking = Booking.objects.get(pk=booking_id) - host = ResourceQuery.get(bundle=booking.resource, template__resource__name=name) - models = self.repo_get(self.repo.SNAPSHOT_MODELS, {}) - if "host" not in models: - models['host'] = host - if 'snapshot' not in models: - models['snapshot'] = Image() - self.repo_put(self.repo.SNAPSHOT_MODELS, models) - self.repo_put(self.repo.SNAPSHOT_BOOKING_ID, booking_id) - - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - snap_confirm = confirm.get("snapshot", {}) - snap_confirm['host'] = name - confirm['snapshot'] = snap_confirm - self.repo_put(self.repo.CONFIRMATION, confirm) - self.set_valid("Success") - - -class Image_Meta_Step(WorkflowStep): - template = "snapshot_workflow/steps/meta.html" - title = "Additional Information" - description = "We need some more info" - short_title = "info" - - def get_context(self): - context = super(Image_Meta_Step, self).get_context() - name = self.repo_get(self.repo.SNAPSHOT_NAME, False) - desc = self.repo_get(self.repo.SNAPSHOT_DESC, False) - form = None - if name and desc: - form = BasicMetaForm(initial={"name": name, "description": desc}) - else: - form = BasicMetaForm() - context['form'] = form - return context - - def post(self, post_data, user): - form = BasicMetaForm(post_data) - if form.is_valid(): - name = form.cleaned_data['name'] - self.repo_put(self.repo.SNAPSHOT_NAME, name) - description = form.cleaned_data['description'] - self.repo_put(self.repo.SNAPSHOT_DESC, description) - - confirm = self.repo_get(self.repo.CONFIRMATION, {}) - snap_confirm = confirm.get("snapshot", {}) - snap_confirm['name'] = name - snap_confirm['description'] = description - confirm['snapshot'] = snap_confirm - self.repo_put(self.repo.CONFIRMATION, confirm) - - self.set_valid("Success") - else: - self.set_invalid("Please Fill out the Form") diff --git a/src/workflow/tests/__init__.py b/src/workflow/tests/__init__.py deleted file mode 100644 index 4f0437d..0000000 --- a/src/workflow/tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron 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/src/workflow/tests/constants.py b/src/workflow/tests/constants.py deleted file mode 100644 index f94a949..0000000 --- a/src/workflow/tests/constants.py +++ /dev/null @@ -1,198 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 -############################################################################## -POD_XML = """<mxGraphModel> -<root> -<mxCell id="0"/> -<mxCell id="1" parent="0"/> -<mxCell id="host_null" value="Test profile 0" style="editable=0" vertex="1" connectable="0" parent="1"> -<mxGeometry x="75" y="150" width="110" height="90" as="geometry"/> -</mxCell> -<mxCell id="2" value="eno0" style="fillColor=blue;editable=0" vertex="1" parent="host_null"> -<mxGeometry x="90" y="5" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="3" value="eno1" style="fillColor=blue;editable=0" vertex="1" parent="host_null"> -<mxGeometry x="90" y="30" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="4" value="eno2" style="fillColor=blue;editable=0" vertex="1" parent="host_null"> -<mxGeometry x="90" y="55" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="5" value="Test profile 3" style="editable=0" vertex="1" connectable="0" parent="1"> -<mxGeometry x="75" y="290" width="110" height="90" as="geometry"/> -</mxCell> -<mxCell id="6" value="eno0" style="fillColor=blue;editable=0" vertex="1" parent="5"> -<mxGeometry x="90" y="5" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="7" value="eno1" style="fillColor=blue;editable=0" vertex="1" parent="5"> -<mxGeometry x="90" y="30" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="8" value="eno2" style="fillColor=blue;editable=0" vertex="1" parent="5"> -<mxGeometry x="90" y="55" width="20" height="20" as="geometry"/> -</mxCell> -<mxCell id="network_0" value="{"vlan_id":"500","name":"net"}" style="fillColor=red" vertex="1" parent="1"> -<mxGeometry x="400" y="-20" width="10" height="2000" as="geometry"/> -</mxCell> -<mxCell id="9" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="10" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.02" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="11" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.04" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="12" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.06" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="13" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.08" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="14" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.1" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="15" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.12" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="16" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.14" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="17" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.16" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="18" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.18" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="19" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.2" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="20" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.22" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="21" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.24" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="22" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.26" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="23" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.28" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="24" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.3" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="25" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.32" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="26" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.34" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="27" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.36" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="28" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.38" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="29" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.4" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="30" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.42" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="31" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.44" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="32" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.46" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="33" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.48" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="34" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.5" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="35" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.52" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="36" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.54" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="37" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.56" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="38" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.58" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="39" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.6" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="40" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.62" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="41" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.64" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="42" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.66" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="43" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.68" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="44" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.7000000000000001" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="45" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.72" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="46" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.74" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="47" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.76" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="48" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.78" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="49" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.8" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="50" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.8200000000000001" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="51" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.84" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="52" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.86" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="53" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.88" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="54" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.9" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="55" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.92" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="56" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.9400000000000001" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="57" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.96" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="58" value="" style="fillColor=black;opacity=0" vertex="1" parent="network_0"> -<mxGeometry y="0.98" width="10" height="40" relative="1" as="geometry"/> -</mxCell> -<mxCell id="59" value="{"tagged":true}" style="strokeColor=red" edge="1" parent="1" source="2" target="13"> -<mxGeometry relative="1" as="geometry"/> -</mxCell> -<mxCell id="60" value="{"tagged":false}" style="strokeColor=red" edge="1" parent="1" source="7" target="17"> -<mxGeometry relative="1" as="geometry"/> -</mxCell> -</root> -</mxGraphModel> -""" diff --git a/src/workflow/tests/test_fixtures.py b/src/workflow/tests/test_fixtures.py deleted file mode 100644 index fe16be7..0000000 --- a/src/workflow/tests/test_fixtures.py +++ /dev/null @@ -1,2 +0,0 @@ - -MX_GRAPH_MODEL = '<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="host_c" value="{"name":"c","description":"Intel based ProLiant server from HPE"}" style="editable=0" parent="1" vertex="1" connectable="0"><mxGeometry x="75" y="150" width="110" height="175" as="geometry"><mxPoint x="-50" as="offset"/></mxGeometry></mxCell><mxCell id="2" value="{"name":"ens4f1","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="12" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="3" value="{"name":"ens4f0","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="37" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="4" value="{"name":"ens1f2","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="62" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="5" value="{"name":"ens1f1","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="87" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="6" value="{"name":"ens1f0","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="112" width="20" height="20" as="geometry"><mxPoint x="-26" as="offset"/></mxGeometry></mxCell><mxCell id="7" value="{"name":"eno49","description":"speed: 10000M type: onboard"}" style="fillColor=blue;editable=0" parent="host_c" vertex="1"><mxGeometry x="90" y="137" width="20" height="20" as="geometry"><mxPoint x="-22" as="offset"/></mxGeometry></mxCell><mxCell id="network_0" value="{"name":"public","public":true}" style="fillColor=red" parent="1" vertex="1"><mxGeometry x="400" y="-10" width="10" height="1700" as="geometry"/></mxCell><mxCell id="8" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="9" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.022222222222222223" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="10" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.044444444444444446" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="11" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.06666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="12" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.08888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="13" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.11111111111111112" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="14" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.13333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="15" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.15555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="16" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.17777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="17" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.2" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="18" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.22222222222222224" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="19" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.24444444444444446" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="20" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.26666666666666666" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="21" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.2888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="22" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.3111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="23" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.33333333333333337" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="24" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.35555555555555557" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="25" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.37777777777777777" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="26" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="27" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4222222222222222" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="28" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4444444444444445" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="29" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.4666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="30" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.48888888888888893" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="31" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5111111111111112" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="32" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="33" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="34" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.5777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="35" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="36" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6222222222222222" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="37" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6444444444444445" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="38" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="39" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.6888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="40" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="41" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7333333333333334" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="42" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7555555555555555" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="43" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.7777777777777778" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="44" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="45" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8222222222222223" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="46" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8444444444444444" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="47" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.8666666666666667" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="48" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.888888888888889" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="49" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9111111111111111" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="50" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9333333333333333" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="51" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9555555555555556" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="52" value="" style="fillColor=black;opacity=0" parent="network_0" vertex="1"><mxGeometry y="0.9777777777777779" width="10" height="37.77777777777778" relative="1" as="geometry"/></mxCell><mxCell id="53" value="{"tagged":false}" style="strokeColor=red" parent="1" source="2" target="13" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel>' diff --git a/src/workflow/tests/test_steps.py b/src/workflow/tests/test_steps.py deleted file mode 100644 index 57bf6a3..0000000 --- a/src/workflow/tests/test_steps.py +++ /dev/null @@ -1,269 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 -############################################################################## - -""" -This file tests basic functionality of each step class. - -More in depth case coverage of WorkflowStep.post() must happen elsewhere. -""" - -import json -from unittest import SkipTest, mock - -from django.test import TestCase, RequestFactory -from dashboard.testing_utils import make_lab, make_user, make_os,\ - make_complete_host_profile, make_opnfv_role, make_image, make_grb,\ - make_config_bundle, make_host, make_user_profile, make_generic_host -from workflow import resource_bundle_workflow -from workflow import booking_workflow -from workflow import sw_bundle_workflow -from workflow.models import Repository -from workflow.tests import test_fixtures - - -class TestConfig: - """ - Basic class to instantiate and hold reference. - - to models we will need often - """ - - def __init__(self, usr=None): - self.lab = make_lab() - self.user = usr or make_user() - self.os = make_os() - self.host_prof = make_complete_host_profile(self.lab) - self.host = make_host(self.host_prof, self.lab, name="host1") - - # pod description as required by testing lib - self.topology = { - "host1": { - "type": self.host_prof, - "role": make_opnfv_role(), - "image": make_image(self.lab, 3, self.user, self.os, self.host_prof), - "nets": [ - [{"name": "public", "tagged": True, "public": True}] - ] - } - } - self.grb = make_grb(self.topology, self.user, self.lab)[0] - self.generic_host = make_generic_host(self.grb, self.host_prof, "host1") - - -class StepTestCase(TestCase): - - # after setUp is called, this should be an instance of a step - step = None - - post_data = {} # subclasses will set this - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - cls.factory = RequestFactory() - cls.user_prof = make_user_profile() - cls.user = cls.user_prof.user - - def setUp(self): - super().setUp() - if self.step is None: - raise SkipTest("Step instance not given") - repo = Repository() - self.add_to_repo(repo) - self.step = self.step(1, repo) - - def assertCorrectPostBehavior(self, post_data): - """ - Stub for validating step behavior on POST request. - - allows subclasses to override and make assertions about - the side effects of self.step.post() - post_data is the data passed into post() - """ - return - - def add_to_repo(self, repo): - """ - Stub for modifying the step's repo. - - This method is a hook that allows subclasses to modify - the contents of the repo before the step is created. - """ - return - - def assertValidHtml(self, html_str): - """ - Assert that html_str is a valid html fragment. - - However, I know of no good way of doing this in python - """ - self.assertTrue(isinstance(html_str, str)) - self.assertGreater(len(html_str), 0) - - def test_render_to_string(self): - request = self.factory.get("/workflow/manager/") - request.user = self.user - response_html = self.step.render_to_string(request) - self.assertValidHtml(response_html) - - def test_post(self, data=None): - post_data = data or self.post_data - self.step.post(post_data, self.user) - self.assertCorrectPostBehavior(data) - - -class SelectStepTestCase(StepTestCase): - # ID of model to be sent to the step's form - # can be an int or a list of ints - obj_id = -1 - - def setUp(self): - super().setUp() - - try: - iter(self.obj_id) - except TypeError: - self.obj_id = [self.obj_id] - - field_data = json.dumps(self.obj_id) - self.post_data = { - "searchable_select": [field_data] - } - - -class DefineHardwareTestCase(StepTestCase): - step = resource_bundle_workflow.Define_Hardware - post_data = { - "filter_field": { - "lab": { - "lab_35": {"selected": True, "id": 35}}, - "host": { - "host_1": {"selected": True, "id": 1}} - } - } - - -class DefineNetworkTestCase(StepTestCase): - step = resource_bundle_workflow.Define_Nets - post_data = {"xml": test_fixtures.MX_GRAPH_MODEL} - - -class ResourceMetaTestCase(StepTestCase): - step = resource_bundle_workflow.Resource_Meta_Info - post_data = { - "bundle_name": "my_bundle", - "bundle_description": "My Bundle" - } - - -class BookingResourceTestCase(SelectStepTestCase): - step = booking_workflow.Booking_Resource_Select - - def add_to_repo(self, repo): - repo.el[repo.SESSION_USER] = self.user - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - conf = TestConfig(usr=cls.user) - cls.obj_id = conf.grb.id - - -class SoftwareSelectTestCase(SelectStepTestCase): - step = booking_workflow.SWConfig_Select - - def add_to_repo(self, repo): - repo.el[repo.SESSION_USER] = self.user - repo.el[repo.SELECTED_RESOURCE_TEMPLATE] = self.conf.grb - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - cls.conf = TestConfig(usr=cls.user) - host_map = {"host1": cls.conf.generic_host} - config_bundle = make_config_bundle(cls.conf.grb, cls.conf.user, cls.conf.topology, host_map)[0] - cls.obj_id = config_bundle.id - - -class OPNFVSelectTestCase(SelectStepTestCase): - step = booking_workflow.OPNFV_Select - - def add_to_repo(self, repo): - repo.el[repo.SELECTED_CONFIG_BUNDLE] = self.config_bundle - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - conf = TestConfig(usr=cls.user) - host_map = {"host1": conf.generic_host} - cls.config_bundle, opnfv_config = make_config_bundle(conf.grb, conf.user, conf.topology, host_map) - cls.obj_id = opnfv_config.id - - -class BookingMetaTestCase(StepTestCase): - step = booking_workflow.Booking_Meta - post_data = { - "length": 14, - "purpose": "Testing", - "project": "Lab as a Service", - "users": ["[-1]"] - } - - def add_to_repo(self, repo): - repo.el[repo.SESSION_MANAGER] = mock.MagicMock() - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - new_user = make_user(username="collaborator", email="different@mail.com") - new_user_prof = make_user_profile(user=new_user) - data = "[" + str(new_user_prof.id) + "]" # list of IDs - cls.post_data['users'] = [data] - - -class ConfigResourceSelectTestCase(SelectStepTestCase): - step = sw_bundle_workflow.SWConf_Resource_Select - - def add_to_repo(self, repo): - repo.el[repo.SESSION_USER] = self.user - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - conf = TestConfig(usr=cls.user) - cls.obj_id = conf.grb.id - - -class DefineSoftwareTestCase(StepTestCase): - step = sw_bundle_workflow.Define_Software - post_data = { - "form-0-image": 1, - "headnode": 1, - "form-0-headnode": "", - "form-TOTAL_FORMS": 1, - "form-INITIAL_FORMS": 1, - "form-MIN_NUM_FORMS": 0, - "form-MAX_NUM_FORMS": 1000, - } - - def add_to_repo(self, repo): - repo.el[repo.SELECTED_RESOURCE_TEMPLATE] = self.conf.grb - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - cls.conf = TestConfig(usr=cls.user) - - -class ConfigSoftwareTestCase(StepTestCase): - step = sw_bundle_workflow.Config_Software - post_data = { - "name": "config_bundle", - "description": "My Config Bundle" - } diff --git a/src/workflow/tests/test_workflows.py b/src/workflow/tests/test_workflows.py deleted file mode 100644 index 995d699..0000000 --- a/src/workflow/tests/test_workflows.py +++ /dev/null @@ -1,99 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, 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 unittest import SkipTest -from django.test import TestCase -from workflow.workflow_factory import WorkflowFactory - - -""" -To start a workflow: - POST to /wf/workflow {"add": <wf_type_int> - - types: - 0 - Booking - 1 - Resource - 2 - Config - -To remove a workflow: - POST to /wf/workflow {"cancel": ""} -""" - - -class WorkflowTestCase(TestCase): - - @classmethod - def setUpClass(cls): - super().setUpClass() - raise SkipTest("These tests are no good") - - def setUp(self): - self.clear_workflow() - self.create_workflow(self.wf_type) - - def create_workflow(self, wf_type): - self.clear_workflow() - - # creates workflow on backend - self.client.post("/", {"create": int(wf_type)}) # TODO: verify content type, etc - - def clear_workflow(self): - session = self.client.session - for k in session.keys(): - del session[k] - session.save() - - def render_steps(self): - """Retrieve each step individually at /wf/workflow/step=<index>.""" - for i in range(self.step_count): - # renders the step itself, not in an iframe - exception = None - try: - response = self.client.get("/wf/workflow/", {"step": str(i)}) - self.assertLess(response.status_code, 300) - except Exception as e: - exception = e - - self.assertIsNone(exception) - - -class BookingWorkflowTestCase(WorkflowTestCase): - - @classmethod - def setUpClass(cls): - super(BookingWorkflowTestCase, cls).setUpClass() - cls.step_count = len(WorkflowFactory.booking_steps) - cls.wf_type = 0 - - def test_steps_render(self): - super(BookingWorkflowTestCase, self).render_steps() - - -class ResourceWorkflowTestCase(WorkflowTestCase): - - @classmethod - def setUpClass(cls): - super(ResourceWorkflowTestCase, cls).setUpClass() - cls.step_count = len(WorkflowFactory.resource_steps) - cls.wf_type = 1 - - def test_steps_render(self): - super(ResourceWorkflowTestCase, self).render_steps() - - -class ConfigWorkflowTestCase(WorkflowTestCase): - - @classmethod - def setUpClass(cls): - super(ConfigWorkflowTestCase, cls).setUpClass() - cls.step_count = len(WorkflowFactory.config_steps) - cls.wf_type = 2 - - def test_steps_render(self): - super(ConfigWorkflowTestCase, self).render_steps() diff --git a/src/workflow/urls.py b/src/workflow/urls.py index b1b95a7..e0ee41d 100644 --- a/src/workflow/urls.py +++ b/src/workflow/urls.py @@ -9,15 +9,10 @@ from django.conf.urls import url - -from workflow.views import manager_view, viewport_view, add_workflow, remove_workflow, create_workflow +from workflow.views import design_a_pod_view, book_a_pod_view app_name = 'workflow' urlpatterns = [ - - url(r'^manager/$', manager_view, name='manager'), - url(r'^add/$', add_workflow, name='add_workflow'), - url(r'^create/$', create_workflow, name='create_workflow'), - url(r'^pop/$', remove_workflow, name='remove_workflow'), - url(r'^$', viewport_view, name='viewport') + url(r'^design/$', design_a_pod_view, name='design_a_pod'), + url(r'^book/$', book_a_pod_view, name='book_a_pod'), ] diff --git a/src/workflow/views.py b/src/workflow/views.py index fb311b7..08ed22b 100644 --- a/src/workflow/views.py +++ b/src/workflow/views.py @@ -7,101 +7,13 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## - -from django.http import HttpResponse +import json from django.shortcuts import render -from account.models import Lab - -import uuid - -from workflow.workflow_manager import ManagerTracker, SessionManager - -import logging -logger = logging.getLogger(__name__) - - -def attempt_auth(request): - try: - manager = ManagerTracker.managers[request.session['manager_session']] - - return manager - - except KeyError: - return None - - -def remove_workflow(request): - manager = attempt_auth(request) - - if not manager: - return no_workflow(request) - - has_more_workflows, result = manager.pop_workflow(discard=True) - - if not has_more_workflows: # this was the last workflow, so delete the reference to it in the tracker - del ManagerTracker.managers[request.session['manager_session']] - return manager.render(request) - - -def add_workflow(request): - manager = attempt_auth(request) - if not manager: - return no_workflow(request) - try: - workflow_type = int(request.POST.get('workflow_type')) - except ValueError: - return HttpResponse(status=400) - - manager.add_workflow(workflow_type=workflow_type) - return manager.render(request) # do we want this? - - -def manager_view(request): - manager = attempt_auth(request) - if not manager: - return no_workflow(request) - - return manager.handle_request(request) - - -def viewport_view(request): - if not request.user.is_authenticated: - return login(request) - - manager = attempt_auth(request) - if manager is None: - return no_workflow(request) - - if request.method != 'GET': - return HttpResponse(status=405) - - context = { - 'contact_email': Lab.objects.get(name="UNH_IOL").contact_email - } - - return render(request, 'workflow/viewport-base.html', context) - - -def create_workflow(request): - if request.method != 'POST': - return HttpResponse(status=405) - workflow_type = request.POST.get('workflow_type') - try: - workflow_type = int(workflow_type) - except Exception: - return HttpResponse(status=400) - mgr_uuid = create_session(workflow_type, request=request,) - request.session['manager_session'] = mgr_uuid - return HttpResponse() - - -def create_session(wf_type, request): - smgr = SessionManager(request=request) - smgr.add_workflow(workflow_type=wf_type, target_id=request.POST.get("target")) - manager_uuid = uuid.uuid4().hex - ManagerTracker.getInstance().managers[manager_uuid] = smgr - - return manager_uuid +from laas_dashboard.settings import TEMPLATE_OVERRIDE +from django.http import HttpResponse +from django.http.response import JsonResponse +from workflow.forms import BookingMetaForm +from api.views import liblaas_request, make_booking def no_workflow(request): @@ -110,3 +22,40 @@ def no_workflow(request): def login(request): return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + +def design_a_pod_view(request): + if request.method == "GET": + if not request.user.is_authenticated: + return login(request) + template = "workflow/design_a_pod.html" + context = { + "dashboard": str(TEMPLATE_OVERRIDE) + } + return render(request, template, context) + + if request.method == "POST": + print("forwarding request to liblaas...") + return liblaas_request(request) + + return HttpResponse(status=405) + +def book_a_pod_view(request): + if request.method == "GET": + if not request.user.is_authenticated: + return login(request) + template = "workflow/book_a_pod.html" + context = { + "dashboard": str(TEMPLATE_OVERRIDE), + "form": BookingMetaForm(initial={}, user_initial=[], owner=request.user), + } + return render(request, template, context) + + if request.method == "POST": + print("forwarding request to liblaas...") + return liblaas_request(request) + + # Using PUT to signal that we do not want to talk to liblaas + if request.method == "PUT": + return make_booking(request) + + return HttpResponse(status=405) diff --git a/src/workflow/workflow_factory.py b/src/workflow/workflow_factory.py deleted file mode 100644 index e688510..0000000 --- a/src/workflow/workflow_factory.py +++ /dev/null @@ -1,126 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, 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 workflow.booking_workflow import Booking_Resource_Select, Booking_Meta, OPNFV_Select -from workflow.resource_bundle_workflow import Define_Hardware, Define_Nets, Resource_Meta_Info, Define_Software -from workflow.snapshot_workflow import Select_Host_Step, Image_Meta_Step -from workflow.opnfv_workflow import Pick_Installer, Assign_Network_Roles, Assign_Host_Roles, OPNFV_Resource_Select, MetaInfo -from workflow.models import Confirmation_Step - -import uuid - -import logging -logger = logging.getLogger(__name__) - - -class MetaStep(object): - - UNTOUCHED = 0 - INVALID = 100 - VALID = 200 - - def set_invalid(self, message, code=100): - self.valid = code - self.message = message - - def set_valid(self, message, code=200): - self.valid = code - self.message = message - - def __init__(self, *args, **kwargs): - self.short_title = "error" - self.skip_step = 0 - self.valid = 0 - self.hidden = False - self.message = "" - self.id = uuid.uuid4() - - def to_json(self): - return { - 'title': self.short_title, - 'skip': self.skip_step, - 'valid': self.valid, - 'message': self.message, - } - - def __str__(self): - return "metastep: " + str(self.short_title) - - def __hash__(self): - return hash(self.id) - - def __eq__(self, other): - return self.id.int == other.id.int - - def __ne__(self, other): - return self.id.int != other.id.int - - -class Workflow(object): - def __init__(self, steps, repository): - self.repository = repository - self.steps = steps - self.active_index = 0 - - -class WorkflowFactory(): - booking_steps = [ - Booking_Resource_Select, - Booking_Meta, - OPNFV_Select, - ] - - resource_steps = [ - Define_Hardware, - Define_Software, - Define_Nets, - Resource_Meta_Info, - ] - - snapshot_steps = [ - Select_Host_Step, - Image_Meta_Step, - ] - - opnfv_steps = [ - OPNFV_Resource_Select, - Pick_Installer, - Assign_Network_Roles, - Assign_Host_Roles, - MetaInfo - ] - - def conjure(self, workflow_type=None, repo=None): - workflow_types = [ - self.booking_steps, - self.resource_steps, - self.snapshot_steps, - self.opnfv_steps, - ] - - steps = self.make_steps(workflow_types[workflow_type], repository=repo) - return steps - - def create_workflow(self, workflow_type=None, repo=None): - steps = self.conjure(workflow_type, repo) - c_step = self.make_step(Confirmation_Step, repo) - steps.append(c_step) - return Workflow(steps, repo) - - def make_steps(self, step_types, repository): - steps = [] - for step_type in step_types: - steps.append(self.make_step(step_type, repository)) - - return steps - - def make_step(self, step_type, repository): - iden = step_type.description + step_type.title + step_type.template - return step_type(iden, repository) diff --git a/src/workflow/workflow_manager.py b/src/workflow/workflow_manager.py deleted file mode 100644 index 40be9d6..0000000 --- a/src/workflow/workflow_manager.py +++ /dev/null @@ -1,270 +0,0 @@ -############################################################################## -# Copyright (c) 2018 Sawyer Bergeron, 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 -############################################################################## - - -from django.http import JsonResponse -from django.http.request import QueryDict -from django.urls import reverse - -from booking.models import Booking -from workflow.workflow_factory import WorkflowFactory -from workflow.models import Repository -from resource_inventory.models import ( - ResourceTemplate, - ResourceConfiguration, - OPNFVConfig -) -from workflow.forms import ManagerForm - -import logging -logger = logging.getLogger(__name__) - - -class SessionManager(): - def active_workflow(self): - return self.workflows[-1] - - def __init__(self, request=None): - self.workflows = [] - self.owner = request.user - self.factory = WorkflowFactory() - self.result = None - - def set_step_statuses(self, superclass_type, desired_enabled=True): - workflow = self.active_workflow() - steps = workflow.steps - for step in steps: - if isinstance(step, superclass_type): - if desired_enabled: - step.enable() - else: - step.disable() - - def add_workflow(self, workflow_type=None, **kwargs): - repo = Repository() - if (len(self.workflows) >= 1): - defaults = self.workflows[-1].repository.get_child_defaults() - repo.set_defaults(defaults) - repo.el[repo.HAS_RESULT] = False - repo.el[repo.SESSION_USER] = self.owner - repo.el[repo.SESSION_MANAGER] = self - self.workflows.append( - self.factory.create_workflow( - workflow_type=workflow_type, - repo=repo - ) - ) - - def get_redirect(self): - if isinstance(self.result, Booking): - return reverse('booking:booking_detail', kwargs={'booking_id': self.result.id}) - return "/" - - def pop_workflow(self, discard=False): - multiple_wfs = len(self.workflows) > 1 - if multiple_wfs: - if self.workflows[-1].repository.el[Repository.RESULT]: # move result - key = self.workflows[-1].repository.el[Repository.RESULT_KEY] - result = self.workflows[-1].repository.el[Repository.RESULT] - self.workflows[-2].repository.el[key] = result - prev_workflow = self.workflows.pop() - if self.workflows: - current_repo = self.workflows[-1].repository - else: - current_repo = prev_workflow.repository - self.result = current_repo.el[current_repo.RESULT] - if discard: - current_repo.cancel() - return multiple_wfs, self.result - - def status(self, request): - return { - "steps": [step.to_json() for step in self.active_workflow().steps], - "active": self.active_workflow().repository.el['active_step'], - "workflow_count": len(self.workflows) - } - - def handle_post(self, request): - form = ManagerForm(request.POST) - if form.is_valid(): - self.get_active_step().post( - QueryDict(form.cleaned_data['step_form']), - user=request.user - ) - # change step - if form.cleaned_data['step'] == 'prev': - self.go_prev() - if form.cleaned_data['step'] == 'next': - self.go_next() - else: - pass # Exception? - - def handle_request(self, request): - if request.method == 'POST': - self.handle_post(request) - return self.render(request) - - def render(self, request, **kwargs): - if self.workflows: - return JsonResponse({ - "meta": self.status(request), - "content": self.get_active_step().render_to_string(request), - }) - else: - return JsonResponse({ - "redirect": self.get_redirect() - }) - - def post_render(self, request): - return self.active_workflow().steps[self.active_workflow().active_index].post_render(request) - - def get_active_step(self): - return self.active_workflow().steps[self.active_workflow().active_index] - - def go_next(self, **kwargs): - # need to verify current step is valid to allow this - if self.get_active_step().valid < 200: - return - next_step = self.active_workflow().active_index + 1 - if next_step >= len(self.active_workflow().steps): - raise Exception("Out of bounds request for step") - while not self.active_workflow().steps[next_step].enabled: - next_step += 1 - self.active_workflow().repository.el['active_step'] = next_step - self.active_workflow().active_index = next_step - - def go_prev(self, **kwargs): - prev_step = self.active_workflow().active_index - 1 - if prev_step < 0: - raise Exception("Out of bounds request for step") - while not self.active_workflow().steps[prev_step].enabled: - prev_step -= 1 - self.active_workflow().repository.el['active_step'] = prev_step - self.active_workflow().active_index = prev_step - - def prefill_repo(self, target_id, workflow_type): - self.repository.el[self.repository.EDIT] = True - edit_object = None - if workflow_type == 0: - edit_object = Booking.objects.get(pk=target_id) - self.prefill_booking(edit_object) - elif workflow_type == 1: - edit_object = ResourceTemplate.objects.get(pk=target_id) - self.prefill_resource(edit_object) - elif workflow_type == 2: - edit_object = ResourceTemplate.objects.get(pk=target_id) - self.prefill_config(edit_object) - - def prefill_booking(self, booking): - models = self.make_booking_models(booking) - confirmation = self.make_booking_confirm(booking) - self.active_workflow().repository.el[self.active_workflow().repository.BOOKING_MODELS] = models - self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirmation - self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = self.make_grb_models(booking.resource.template) - self.active_workflow().repository.el[self.active_workflow().repository.SELECTED_RESOURCE_TEMPLATE] = self.make_grb_models(booking.resource.template)['bundle'] - self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = self.make_config_models(booking.config_bundle) - - def prefill_resource(self, resource): - models = self.make_grb_models(resource) - confirm = self.make_grb_confirm(resource) - self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = models - self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm - - def prefill_config(self, config): - models = self.make_config_models(config) - confirm = self.make_config_confirm(config) - self.active_workflow().repository.el[self.active_workflow().repository.CONFIG_MODELS] = models - self.active_workflow().repository.el[self.active_workflow().repository.CONFIRMATION] = confirm - grb_models = self.make_grb_models(config.bundle) - self.active_workflow().repository.el[self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS] = grb_models - - def make_grb_models(self, resource): - models = self.active_workflow().repository.el.get(self.active_workflow().repository.RESOURCE_TEMPLATE_MODELS, {}) - models['hosts'] = [] - models['bundle'] = resource - models['interfaces'] = {} - models['vlans'] = {} - for host in resource.getResources(): - models['hosts'].append(host) - models['interfaces'][host.resource.name] = [] - models['vlans'][host.resource.name] = {} - for interface in host.generic_interfaces.all(): - models['interfaces'][host.resource.name].append(interface) - models['vlans'][host.resource.name][interface.profile.name] = [] - for vlan in interface.vlans.all(): - models['vlans'][host.resource.name][interface.profile.name].append(vlan) - return models - - def make_grb_confirm(self, resource): - confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) - confirm['resource'] = {} - confirm['resource']['hosts'] = [] - confirm['resource']['lab'] = resource.lab.lab_user.username - for host in resource.getResources(): - confirm['resource']['hosts'].append({"name": host.resource.name, "profile": host.profile.name}) - return confirm - - def make_config_models(self, config): - models = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIG_MODELS, {}) - models['bundle'] = config - models['host_configs'] = [] - for host_conf in ResourceConfiguration.objects.filter(bundle=config): - models['host_configs'].append(host_conf) - models['opnfv'] = OPNFVConfig.objects.filter(bundle=config).last() - return models - - def make_config_confirm(self, config): - confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) - confirm['configuration'] = {} - confirm['configuration']['hosts'] = [] - confirm['configuration']['name'] = config.name - confirm['configuration']['description'] = config.description - opnfv = OPNFVConfig.objects.filter(bundle=config).last() - confirm['configuration']['installer'] = opnfv.installer.name - confirm['configuration']['scenario'] = opnfv.scenario.name - for host_conf in ResourceConfiguration.objects.filter(bundle=config): - h = {"name": host_conf.host.resource.name, "image": host_conf.image.name, "role": host_conf.opnfvRole.name} - confirm['configuration']['hosts'].append(h) - return confirm - - def make_booking_models(self, booking): - models = self.active_workflow().repository.el.get(self.active_workflow().repository.BOOKING_MODELS, {}) - models['booking'] = booking - models['collaborators'] = [] - for user in booking.collaborators.all(): - models['collaborators'].append(user) - return models - - def make_booking_confirm(self, booking): - confirm = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIRMATION, {}) - confirm['booking'] = {} - confirm['booking']['length'] = (booking.end - booking.start).days - confirm['booking']['project'] = booking.project - confirm['booking']['purpose'] = booking.purpose - confirm['booking']['resource name'] = booking.resource.template.name - confirm['booking']['configuration name'] = booking.config_bundle.name - confirm['booking']['collaborators'] = [] - for user in booking.collaborators.all(): - confirm['booking']['collaborators'].append(user.username) - return confirm - - -class ManagerTracker(): - instance = None - - managers = {} - - def __init__(self): - pass - - @staticmethod - def getInstance(): - if ManagerTracker.instance is None: - ManagerTracker.instance = ManagerTracker() - return ManagerTracker.instance |