diff options
Diffstat (limited to 'dashboard/src/api')
-rw-r--r-- | dashboard/src/api/admin.py | 28 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0001_initial.py | 185 | ||||
-rw-r--r-- | dashboard/src/api/migrations/__init__.py | 4 | ||||
-rw-r--r-- | dashboard/src/api/models.py | 713 | ||||
-rw-r--r-- | dashboard/src/api/serializers.py | 53 | ||||
-rw-r--r-- | dashboard/src/api/serializers/__init__.py | 8 | ||||
-rw-r--r-- | dashboard/src/api/serializers/booking_serializer.py | 156 | ||||
-rw-r--r-- | dashboard/src/api/serializers/old_serializers.py | 28 | ||||
-rw-r--r-- | dashboard/src/api/tests/__init__.py | 8 | ||||
-rw-r--r-- | dashboard/src/api/tests/test_serializers.py | 213 | ||||
-rw-r--r-- | dashboard/src/api/urls.py | 13 | ||||
-rw-r--r-- | dashboard/src/api/views.py | 101 |
12 files changed, 1433 insertions, 77 deletions
diff --git a/dashboard/src/api/admin.py b/dashboard/src/api/admin.py new file mode 100644 index 0000000..f1bc70a --- /dev/null +++ b/dashboard/src/api/admin.py @@ -0,0 +1,28 @@ +############################################################################## +# Copyright (c) 2016 Max Breitenfeldt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +from django.apps import AppConfig +from django.contrib import admin + +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(AccessRelation) +admin.site.register(SoftwareRelation) +admin.site.register(HostHardwareRelation) +admin.site.register(HostNetworkRelation) diff --git a/dashboard/src/api/migrations/0001_initial.py b/dashboard/src/api/migrations/0001_initial.py new file mode 100644 index 0000000..abe6f5e --- /dev/null +++ b/dashboard/src/api/migrations/0001_initial.py @@ -0,0 +1,185 @@ +############################################################################## +# 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 +############################################################################## +# Generated by Django 2.1 on 2018-09-14 14:48 + +import api.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('booking', '__first__'), + ('resource_inventory', '__first__'), + ] + + operations = [ + migrations.CreateModel( + name='AccessRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HostHardwareRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Host')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HostNetworkRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Host')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('delta', models.TextField()), + ('complete', models.BooleanField(default=False)), + ('booking', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='booking.Booking')), + ], + ), + migrations.CreateModel( + name='OpnfvApiConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('installer', models.CharField(max_length=100)), + ('scenario', models.CharField(max_length=100)), + ('delta', models.TextField()), + ('roles', models.ManyToManyField(to='resource_inventory.Host')), + ], + ), + migrations.CreateModel( + name='SoftwareRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TaskConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='AccessConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('access_type', models.CharField(max_length=50)), + ('revoke', models.BooleanField(default=False)), + ('context', models.TextField(default='')), + ('delta', models.TextField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='HardwareConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('image', models.CharField(default='defimage', max_length=100)), + ('power', models.CharField(default='off', max_length=100)), + ('hostname', models.CharField(default='hostname', max_length=100)), + ('ipmi_create', models.BooleanField(default=False)), + ('delta', models.TextField()), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='NetworkConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('delta', models.TextField()), + ('interfaces', models.ManyToManyField(to='resource_inventory.Interface')), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='SoftwareConfig', + fields=[ + ('taskconfig_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='api.TaskConfig')), + ('opnfv', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.OpnfvApiConfig')), + ], + bases=('api.taskconfig',), + ), + migrations.AddField( + model_name='hostnetworkrelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='hosthardwarerelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='accessrelation', + name='job', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job'), + ), + migrations.AddField( + model_name='softwarerelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.SoftwareConfig'), + ), + migrations.AddField( + model_name='hostnetworkrelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.NetworkConfig'), + ), + migrations.AddField( + model_name='hosthardwarerelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.HardwareConfig'), + ), + migrations.AddField( + model_name='accessrelation', + name='config', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.AccessConfig'), + ), + ] diff --git a/dashboard/src/api/migrations/__init__.py b/dashboard/src/api/migrations/__init__.py index b5914ce..e0408fa 100644 --- a/dashboard/src/api/migrations/__init__.py +++ b/dashboard/src/api/migrations/__init__.py @@ -1,10 +1,8 @@ ############################################################################## -# 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 ############################################################################## - - diff --git a/dashboard/src/api/models.py b/dashboard/src/api/models.py new file mode 100644 index 0000000..f1e9130 --- /dev/null +++ b/dashboard/src/api/models.py @@ -0,0 +1,713 @@ +############################################################################## +# 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.db import models +from django.core.exceptions import PermissionDenied + +import json +import uuid + +from resource_inventory.models import * +from booking.models import Booking + + +class JobStatus(object): + NEW = 0 + CURRENT = 100 + DONE = 200 + ERROR = 300 + + +class LabManagerTracker(object): + + @classmethod + def get(cls, lab_name, token): + """ + Takes in a lab name (from a url path) + returns a lab manager instance for that lab, if it exists + """ + try: + lab = Lab.objects.get(name=lab_name) + except: + raise PermissionDenied("Lab not found") + if lab.api_token == token: + return LabManager(lab) + raise PermissionDenied("Lab not authorized") + + +class LabManager(object): + """ + This is the class that will ultimately handle all REST calls to + lab endpoints. + handles jobs, inventory, status, etc + may need to create helper classes + """ + + def __init__(self, lab): + self.lab = lab + + def get_profile(self): + prof = {} + prof['name'] = self.lab.name + prof['contact'] = { + "phone": self.lab.contact_phone, + "email": self.lab.contact_email + } + prof['host_count'] = [] + for host in HostProfile.objects.filter(labs=self.lab): + count = Host.objects.filter(profile=host, lab=self.lab).count() + prof['host_count'].append({ + "type": host.name, + "count": count + }) + return prof + + def get_inventory(self): + inventory = {} + hosts = Host.objects.filter(lab=self.lab) + images = Image.objects.filter(from_lab=self.lab) + profiles = HostProfile.objects.filter(labs=self.lab) + inventory['hosts'] = self.serialize_hosts(hosts) + inventory['images'] = self.serialize_images(images) + inventory['host_types'] = self.serialize_host_profiles(profiles) + return inventory + + 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_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_hosts(self, hosts): + host_ser = [] + for host in hosts: + h = {} + h['interfaces'] = [] + h['hostname'] = host.name + h['host_type'] = host.profile.name + for iface in host.interfaces.all(): + eth = {} + eth['mac'] = iface.mac_address + eth['busaddr'] = iface.bus_address + eth['name'] = iface.name + eth['switchport'] = {"switch_name": iface.switch_name, "port_name": iface.port_name} + h['interfaces'].append(eth) + 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_host_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 Job(models.Model): + """ + This is the class that is serialized and put into the api + """ + booking = models.OneToOneField(Booking, on_delete=models.CASCADE, null=True) + status = models.IntegerField(default=JobStatus.NEW) + complete = models.BooleanField(default=False) + + def to_dict(self): + d = {} + j = {} + j['id'] = self.id + for relation in AccessRelation.objects.filter(job=self): + if 'access' not in d: + d['access'] = {} + d['access'][relation.task_id] = relation.config.to_dict() + for relation in SoftwareRelation.objects.filter(job=self): + if 'software' not in d: + d['software'] = {} + d['software'][relation.task_id] = relation.config.to_dict() + for relation in HostHardwareRelation.objects.filter(job=self): + if 'hardware' not in d: + d['hardware'] = {} + d['hardware'][relation.task_id] = relation.config.to_dict() + for relation in HostNetworkRelation.objects.filter(job=self): + if 'network' not in d: + d['network'] = {} + d['network'][relation.task_id] = relation.config.to_dict() + + j['payload'] = d + + return j + + def get_tasklist(self, status="all"): + tasklist = [] + clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation] + if status == "all": + for cls in clist: + tasklist += list(cls.objects.filter(job=self)) + else: + for cls in clist: + tasklist += list(cls.objects.filter(job=self).filter(status=status)) + return tasklist + + def get_delta(self, status): + d = {} + j = {} + j['id'] = self.id + for relation in AccessRelation.objects.filter(job=self).filter(status=status): + if 'access' not in d: + d['access'] = {} + d['access'][relation.task_id] = relation.config.get_delta() + for relation in SoftwareRelation.objects.filter(job=self).filter(status=status): + if 'software' not in d: + d['software'] = {} + d['software'][relation.task_id] = relation.config.get_delta() + for relation in HostHardwareRelation.objects.filter(job=self).filter(status=status): + if 'hardware' not in d: + d['hardware'] = {} + d['hardware'][relation.task_id] = relation.config.get_delta() + for relation in HostNetworkRelation.objects.filter(job=self).filter(status=status): + if 'network' not in d: + d['network'] = {} + d['network'][relation.task_id] = relation.config.get_delta() + + j['payload'] = d + return j + + def to_json(self): + return json.dumps(self.to_dict()) + + +class TaskConfig(models.Model): + def to_dict(self): + pass + + def get_delta(self): + pass + + def to_json(self): + return json.dumps(self.to_dict()) + + def clear_delta(self): + self.delta = '{}' + +class OpnfvApiConfig(models.Model): + + installer = models.CharField(max_length=100) + scenario = models.CharField(max_length=100) + roles = models.ManyToManyField(Host) + delta = models.TextField() + + def to_dict(self): + d = {} + if self.installer: + d['installer'] = self.installer + if self.scenario: + d['scenario'] = self.scenario + + hosts = self.roles.all() + if hosts.exists(): + d['roles'] = [] + for host in self.roles.all(): + d['roles'].append({host.labid: host.config.opnfvRole.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 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): + if not self.delta: + self.delta = self.to_json() + self.save() + return json.loads(self.delta) + +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() + + def to_dict(self): + d = {} + d['access_type'] = self.access_type + d['user'] = self.user.id + d['revoke'] = self.revoke + d['context'] = self.context + return d + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + 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 = context + d = json.loads(self.delta) + d['context'] = context + self.delta = json.dumps(d) + +class SoftwareConfig(TaskConfig): + """ + handled opnfv installations, etc + """ + 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): + """ + handles imaging, user accounts, etc + """ + 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() + + def to_dict(self): + d = {} + d['image'] = self.image + d['power'] = self.power + d['hostname'] = self.hostname + d['ipmi_create'] = str(self.ipmi_create) + d['id'] = self.hosthardwarerelation.host.labid + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + d['lab_token'] = self.hosthardwarerelation.lab_token + return d + + def clear_delta(self): + d = {} + d["id"] = self.hosthardwarerelation.host.labid + d["lab_token"] = self.hosthardwarerelation.lab_token + 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 set_power(self, power): + self.power = power + d = json.loads(self.delta) + d['power'] = power + self.delta = json.dumps(d) + + def set_hostname(self, hostname): + self.hostname = hostname + d = json.loads(self.delta) + d['hostname'] = hostname + self.delta = json.dumps(d) + + def set_ipmi_create(self, ipmi_create): + self.ipmi_create = ipmi_create + d = json.loads(self.delta) + d['ipmi_create'] = ipmi_create + self.delta = json.dumps(d) + + +class NetworkConfig(TaskConfig): + """ + handles network configuration + """ + interfaces = models.ManyToManyField(Interface) + delta = models.TextField() + + def to_dict(self): + d = {} + hid = self.hostnetworkrelation.host.labid + d[hid] = {} + for interface in self.interfaces.all(): + 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}) + + return d + + def to_json(self): + return json.dumps(self.to_dict()) + + def get_delta(self): + if not self.delta: + self.delta = self.to_json() + self.save() + d = json.loads(self.delta) + d['lab_token'] = self.hostnetworkrelation.lab_token + return d + + def clear_delta(self): + pass + + def add_interface(self, interface): + self.interfaces.add(interface) + d = json.loads(self.delta) + hid = self.hostnetworkrelation.host.labid + 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) + + +def get_task(task_id): + for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]: + 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") + + +def get_task_uuid(): + return str(uuid.uuid4()) + + +class TaskRelation(models.Model): + 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="") + + 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) + + 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) + + 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): + host = models.ForeignKey(Host, on_delete=models.CASCADE) + config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE) + + 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) + + +class HostNetworkRelation(TaskRelation): + host = models.ForeignKey(Host, on_delete=models.CASCADE) + config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE) + + def type_str(self): + return "Network Configuration Task" + + def delete(self, *args, **kwargs): + self.config.delete() + return super(self.__class__, self).delete(*args, **kwargs) + + +class JobFactory(object): + + @classmethod + def makeCompleteJob(cls, booking): + hosts = Host.objects.filter(bundle=booking.resource) + job = None + try: + job = Job.objects.get(booking=booking) + except: + job = Job.objects.create(status=JobStatus.NEW, booking=booking) + cls.makeHardwareConfigs( + hosts=hosts, + job=job + ) + cls.makeNetworkConfigs( + hosts=hosts, + job=job + ) + cls.makeSoftware( + hosts=hosts, + job=job + ) + cls.makeAccessConfig( + users=booking.collaborators.all(), + access_type="vpn", + revoke=False, + job=job + ) + cls.makeAccessConfig( + users=[booking.owner], + access_type="vpn", + revoke=False, + job=job + ) + + @classmethod + def makeHardwareConfigs(cls, hosts=[], job=Job()): + for host in hosts: + hardware_config = None + try: + hardware_config = HardwareConfig.objects.get(relation__host=host) + except: + hardware_config = HardwareConfig() + + relation = HostHardwareRelation() + relation.host = host + relation.job = job + relation.config = hardware_config + relation.config.save() + relation.config = relation.config + relation.save() + + hardware_config.clear_delta() + hardware_config.set_image(host.config.image.lab_id) + hardware_config.set_hostname(host.template.resource.name) + hardware_config.set_power("on") + hardware_config.set_ipmi_create(True) + hardware_config.save() + + @classmethod + def makeAccessConfig(cls, users, access_type, revoke=False, job=Job()): + 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() + config.set_access_type(access_type) + config.set_revoke(revoke) + config.set_user(user) + config.save() + + @classmethod + def makeNetworkConfigs(cls, hosts=[], job=Job()): + for host in hosts: + network_config = None + try: + network_config = NetworkConfig.objects.get(relation__host=host) + except: + network_config = NetworkConfig.objects.create() + + relation = HostNetworkRelation() + relation.host = host + relation.job = job + network_config.save() + relation.config = network_config + relation.save() + network_config.clear_delta() + + for interface in host.interfaces.all(): + network_config.add_interface(interface) + network_config.save() + + @classmethod + def makeSoftware(cls, hosts=[], job=Job()): + def init_config(host): + opnfv_config = OpnfvApiConfig() + if host is not None: + opnfv = host.config.bundle.opnfv_config.first() + opnfv_config.installer = opnfv.installer.name + opnfv_config.scenario = opnfv.scenario.name + opnfv_config.save() + return opnfv_config + + try: + host = None + if len(hosts) > 0: + host = hosts[0] + opnfv_config = init_config(host) + + for host in hosts: + opnfv_config.roles.add(host) + software_config = SoftwareConfig.objects.create(opnfv=opnfv_config) + software_config.save() + software_relation = SoftwareRelation.objects.create(job=job, config=software_config) + software_relation.save() + return software_relation + except: + return None + + def makeAccess(cls, user, access_type, revoke): + pass diff --git a/dashboard/src/api/serializers.py b/dashboard/src/api/serializers.py deleted file mode 100644 index 10e1975..0000000 --- a/dashboard/src/api/serializers.py +++ /dev/null @@ -1,53 +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 -from notifier.models import Notifier -from booking.models import Booking -from dashboard.models import Server, Resource, ResourceStatus - -class BookingSerializer(serializers.ModelSerializer): - installer_name = serializers.CharField(source='installer.name') - scenario_name = serializers.CharField(source='scenario.name') - opsys_name = serializers.CharField(source='opsys.name') - - class Meta: - model = Booking - fields = ('id', 'changeid', 'reset', 'user', 'resource_id', 'opsys_name', 'start', 'end', 'installer_name', 'scenario_name', 'purpose') - - -class ServerSerializer(serializers.ModelSerializer): - class Meta: - model = Server - fields = ('id', 'resource_id', 'name', 'model', 'cpu', 'ram', 'storage') - - -class ResourceSerializer(serializers.ModelSerializer): - class Meta: - model = Resource - fields = ('id', 'name', 'description', 'resource_lab', 'url', 'server_set', 'dev_pod') - -class ResourceStatusSerializer(serializers.ModelSerializer): - class Meta: - model = ResourceStatus - fields = ('id', 'resource', 'timestamp','type', 'title', 'content') - -class NotifierSerializer(serializers.ModelSerializer): - class Meta: - model = Notifier - fields = ('id', 'title', 'content', 'user', 'sender', 'message_type', 'msg_sent') - -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/dashboard/src/api/serializers/__init__.py b/dashboard/src/api/serializers/__init__.py new file mode 100644 index 0000000..e0408fa --- /dev/null +++ b/dashboard/src/api/serializers/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# 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/dashboard/src/api/serializers/booking_serializer.py b/dashboard/src/api/serializers/booking_serializer.py new file mode 100644 index 0000000..e891de4 --- /dev/null +++ b/dashboard/src/api/serializers/booking_serializer.py @@ -0,0 +1,156 @@ +############################################################################## +# 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 * + +class BookingField(serializers.Field): + + def to_representation(self, booking): + """ + Takes 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] = HostConfiguration.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): + """ + Takes in a dictionary of primitives + Returns 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): + """ + takes 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/dashboard/src/api/serializers/old_serializers.py b/dashboard/src/api/serializers/old_serializers.py new file mode 100644 index 0000000..f50b90b --- /dev/null +++ b/dashboard/src/api/serializers/old_serializers.py @@ -0,0 +1,28 @@ +############################################################################## +# 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 +from notifier.models import Notifier + + +class NotifierSerializer(serializers.ModelSerializer): + class Meta: + model = Notifier + fields = ('id', 'title', 'content', 'user', 'sender', 'message_type', 'msg_sent') + + +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/dashboard/src/api/tests/__init__.py b/dashboard/src/api/tests/__init__.py new file mode 100644 index 0000000..fe2a32d --- /dev/null +++ b/dashboard/src/api/tests/__init__.py @@ -0,0 +1,8 @@ +############################################################################## +# 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 +##############################################################################
\ No newline at end of file diff --git a/dashboard/src/api/tests/test_serializers.py b/dashboard/src/api/tests/test_serializers.py new file mode 100644 index 0000000..c49010c --- /dev/null +++ b/dashboard/src/api/tests/test_serializers.py @@ -0,0 +1,213 @@ +############################################################################## +# 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 booking.models import Booking +from resource_inventory.models import * +from account.models import Lab +from api.serializers.booking_serializer import * +from datetime import timedelta +from django.utils import timezone +from django.contrib.auth.models import Permission, User + + +class BookingSerializerTestCase(TestCase): + + count = 0 + + def makeHostConfigurations(self, hosts, config): + lab_user = User.objects.create(username="asfasdfasdf") + owner = User.objects.create(username="asfasdfasdffffff") + lab = Lab.objects.create( + lab_user=lab_user, + name="TestLab123123", + contact_email="mail@email.com", + contact_phone="" + ) + jumphost=True + for host in hosts: + image = Image.objects.create( + lab_id=12, + from_lab=lab, + name="this is a test image", + owner=owner + ) + name = "jumphost" + if not jumphost: + name = "compute" + role = OPNFVRole.objects.create( + name=name, + description="stuff" + ) + + HostConfiguration.objects.create( + host=host, + image=image, + bundle=config, + opnfvRole=role + ) + jumphost=False + + + def setUp(self): + self.serializer = BookingField() + lab_user = User.objects.create(username="lab user") + lab = Lab.objects.create(name="test lab", lab_user=lab_user) + # create hostProfile + hostProfile = HostProfile.objects.create( + host_type=0, + name='Test profile', + description='a test profile' + ) + interfaceProfile = InterfaceProfile.objects.create( + speed=1000, + name='eno3', + host=hostProfile + ) + diskProfile = DiskProfile.objects.create( + size=1000, + media_type="SSD", + name='/dev/sda', + host=hostProfile + ) + cpuProfile = CpuProfile.objects.create( + cores=96, + architecture="x86_64", + cpus=2, + host=hostProfile + ) + ramProfile = RamProfile.objects.create( + amount=256, + channels=4, + host=hostProfile + ) + + #create GenericResourceBundle + genericBundle = GenericResourceBundle.objects.create() + + gres1 = GenericResource.objects.create( + bundle=genericBundle, + name='generic resource ' + str(self.count) + ) + self.count += 1 + gHost1 = GenericHost.objects.create( + resource=gres1, + profile=hostProfile + ) + + gres2 = GenericResource.objects.create( + bundle=genericBundle, + name='generic resource ' + str(self.count) + ) + self.count += 1 + gHost2 = GenericHost.objects.create( + resource=gres2, + profile=hostProfile + ) + user1 = User.objects.create(username='user1') + + add_booking_perm = Permission.objects.get(codename='add_booking') + user1.user_permissions.add(add_booking_perm) + + user1 = User.objects.get(pk=user1.id) + + conf = ConfigBundle.objects.create(owner=user1, name="test conf") + self.makeHostConfigurations([gHost1, gHost2], conf) + + #actual resource bundle + bundle = ResourceBundle.objects.create( + template = genericBundle + ) + + host1 = Host.objects.create( + template=gHost1, + booked=True, + name='host1', + bundle=bundle, + profile=hostProfile, + lab=lab + ) + + host2 = Host.objects.create( + template=gHost2, + booked=True, + name='host2', + bundle=bundle, + profile=hostProfile, + lab=lab + ) + + vlan1 = Vlan.objects.create(vlan_id=300, tagged=False) + vlan2 = Vlan.objects.create(vlan_id=300, tagged=False) + + iface1 = Interface.objects.create( + mac_address='00:11:22:33:44:55', + bus_address='some bus address', + switch_name='switch1', + port_name='port10', + host=host1 + ) + + iface1.config = [vlan1] + + iface2 = Interface.objects.create( + mac_address='00:11:22:33:44:56', + bus_address='some bus address', + switch_name='switch1', + port_name='port12', + host=host2 + ) + + iface2.config = [vlan2] + + # finally, can create booking + self.booking = Booking.objects.create( + owner=user1, + start = timezone.now(), + end = timezone.now() + timedelta(weeks=1), + purpose='Testing', + resource=bundle, + config_bundle=conf + ) + + serialized_booking = {} + + host1 = {} + host1['hostname'] = 'host1' + host1['image'] = {} # TODO: Images + host1['deploy_image'] = True + host2 = {} + host2['hostname'] = 'host2' + host2['image'] = {} # TODO: Images + host2['deploy_image'] = True + + serialized_booking['hosts'] = [host1, host2] + + net = {} + net['name'] = 'network_name' + net['vlan_id'] = 300 + netHost1 = {} + netHost1['hostname'] = 'host1' + netHost1['tagged'] = False + netHost1['interface'] = 0 + netHost2 = {} + netHost2['hostname'] = 'host2' + netHost2['tagged'] = False + netHost2['interface'] = 0 + net['hosts'] = [netHost1, netHost2] + + serialized_booking['networking'] = [net] + serialized_booking['jumphost'] = 'host1' + + self.serialized_booking = serialized_booking + + def test_to_representation(self): + keys = ['hosts', 'networking', 'jumphost'] + serialized_form = self.serializer.to_representation(self.booking) + for key in keys: + self.assertEquals(serialized_form[key], self.serialized_booking) diff --git a/dashboard/src/api/urls.py b/dashboard/src/api/urls.py index c2cd510..94f8279 100644 --- a/dashboard/src/api/urls.py +++ b/dashboard/src/api/urls.py @@ -1,5 +1,6 @@ ############################################################################## # Copyright (c) 2016 Max Breitenfeldt and others. +# 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 @@ -24,19 +25,25 @@ Including another URLconf 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include +from django.urls import path from rest_framework import routers from api.views import * router = routers.DefaultRouter() -router.register(r'resources', ResourceViewSet) -router.register(r'servers', ServerViewSet) router.register(r'bookings', BookingViewSet) -router.register(r'resource_status', ResourceStatusViewSet) router.register(r'notifier', NotifierViewSet) router.register(r'user', UserViewSet) urlpatterns = [ url(r'^', include(router.urls)), + 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>/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/new', new_jobs), + path('labs/<slug:lab_name>/jobs/current', current_jobs), + path('labs/<slug:lab_name>/jobs/done', done_jobs), url(r'^token$', GenerateTokenView.as_view(), name='generate_token'), ] diff --git a/dashboard/src/api/views.py b/dashboard/src/api/views.py index b873ef1..cefd131 100644 --- a/dashboard/src/api/views.py +++ b/dashboard/src/api/views.py @@ -1,5 +1,6 @@ ############################################################################## # 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 @@ -8,17 +9,23 @@ ############################################################################## -from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.views import View +from django.http.response import JsonResponse from rest_framework import viewsets from rest_framework.authtoken.models import Token +from django.views.decorators.csrf import csrf_exempt -from api.serializers import * +import json + +from api.serializers.booking_serializer import * +from api.serializers.old_serializers import NotifierSerializer, UserSerializer from account.models import UserProfile from booking.models import Booking -from dashboard.models import Resource, Server, ResourceStatus +from notifier.models import Notifier +from api.models import * class BookingViewSet(viewsets.ModelViewSet): @@ -27,29 +34,16 @@ class BookingViewSet(viewsets.ModelViewSet): filter_fields = ('resource', 'id') -class ServerViewSet(viewsets.ModelViewSet): - queryset = Server.objects.all() - serializer_class = ServerSerializer - filter_fields = ('resource', 'name') - - -class ResourceViewSet(viewsets.ModelViewSet): - queryset = Resource.objects.all() - serializer_class = ResourceSerializer - filter_fields = ('name', 'id') - -class ResourceStatusViewSet(viewsets.ModelViewSet): - queryset = ResourceStatus.objects.all() - serializer_class = ResourceStatusSerializer - class NotifierViewSet(viewsets.ModelViewSet): queryset = Notifier.objects.none() serializer_class = NotifierSerializer + 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): @@ -59,3 +53,74 @@ class GenerateTokenView(View): token.delete() Token.objects.create(user=user) 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) + + +def lab_status(request, lab_name=""): + lab_token = request.META.get('HTTP_AUTH_TOKEN') + lab_manager = LabManagerTracker.get(lab_name, lab_token) + if request.method == "POST": + return JsonResponse(lab_manager.set_status(request.POST), safe=False) + return JsonResponse(lab_manager.get_status(), 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') + task.save() + d = {} + d['task'] = task.config.get_delta() + m = {} + m['status'] = task.status + m['job'] = str(task.job) + m['message'] = task.message + d['meta'] = m + response = json.dumps(d) + return JsonResponse(response) + elif request.method == "GET": + return JsonResponse(get_task(task_id).config.get_delta()) + + +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) + + +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) + + +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) |