diff options
Diffstat (limited to 'dashboard/src/api')
-rw-r--r-- | dashboard/src/api/migrations/0003_auto_20190102_1956.py | 18 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py | 42 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0005_snapshotconfig_delta.py | 18 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0006_auto_20190313_1729.py | 23 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0007_auto_20190417_1511.py | 25 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py | 20 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0008_auto_20190419_1414.py | 28 | ||||
-rw-r--r-- | dashboard/src/api/migrations/0009_merge_20190508_1317.py | 14 | ||||
-rw-r--r-- | dashboard/src/api/models.py | 340 | ||||
-rw-r--r-- | dashboard/src/api/tests/test_models_unittest.py | 269 | ||||
-rw-r--r-- | dashboard/src/api/tests/test_serializers.py | 229 | ||||
-rw-r--r-- | dashboard/src/api/urls.py | 8 | ||||
-rw-r--r-- | dashboard/src/api/views.py | 39 |
13 files changed, 805 insertions, 268 deletions
diff --git a/dashboard/src/api/migrations/0003_auto_20190102_1956.py b/dashboard/src/api/migrations/0003_auto_20190102_1956.py new file mode 100644 index 0000000..2ea5d70 --- /dev/null +++ b/dashboard/src/api/migrations/0003_auto_20190102_1956.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2019-01-02 19:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0002_remove_job_delta'), + ] + + operations = [ + migrations.AlterField( + model_name='accessconfig', + name='delta', + field=models.TextField(default='{}'), + ), + ] diff --git a/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py b/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py new file mode 100644 index 0000000..62bc7af --- /dev/null +++ b/dashboard/src/api/migrations/0004_snapshotconfig_snapshotrelation.py @@ -0,0 +1,42 @@ +# Generated by Django 2.1 on 2019-01-17 15:54 + +import api.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0004_auto_20181017_1532'), + ('api', '0003_auto_20190102_1956'), + ] + + operations = [ + migrations.CreateModel( + name='SnapshotConfig', + 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.IntegerField(null=True)), + ('dashboard_id', models.IntegerField()), + ('host', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.Host')), + ], + bases=('api.taskconfig',), + ), + migrations.CreateModel( + name='SnapshotRelation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.IntegerField(default=0)), + ('task_id', models.CharField(default=api.models.get_task_uuid, max_length=37)), + ('lab_token', models.CharField(default='null', max_length=50)), + ('message', models.TextField(default='')), + ('config', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.SnapshotConfig')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Job')), + ('snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.Image')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/dashboard/src/api/migrations/0005_snapshotconfig_delta.py b/dashboard/src/api/migrations/0005_snapshotconfig_delta.py new file mode 100644 index 0000000..559af90 --- /dev/null +++ b/dashboard/src/api/migrations/0005_snapshotconfig_delta.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2019-01-17 16:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_snapshotconfig_snapshotrelation'), + ] + + operations = [ + migrations.AddField( + model_name='snapshotconfig', + name='delta', + field=models.TextField(default='{}'), + ), + ] diff --git a/dashboard/src/api/migrations/0006_auto_20190313_1729.py b/dashboard/src/api/migrations/0006_auto_20190313_1729.py new file mode 100644 index 0000000..ec148bd --- /dev/null +++ b/dashboard/src/api/migrations/0006_auto_20190313_1729.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1 on 2019-03-13 17:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_snapshotconfig_delta'), + ] + + operations = [ + migrations.AlterField( + model_name='opnfvapiconfig', + name='installer', + field=models.CharField(max_length=200), + ), + migrations.AlterField( + model_name='opnfvapiconfig', + name='scenario', + field=models.CharField(max_length=300), + ), + ] diff --git a/dashboard/src/api/migrations/0007_auto_20190417_1511.py b/dashboard/src/api/migrations/0007_auto_20190417_1511.py new file mode 100644 index 0000000..e7d2c59 --- /dev/null +++ b/dashboard/src/api/migrations/0007_auto_20190417_1511.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1 on 2019-04-17 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_auto_20190313_1729'), + ] + + operations = [ + migrations.AddField( + model_name='opnfvapiconfig', + name='idf', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='opnfvapiconfig', + name='pdf', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py new file mode 100644 index 0000000..46f3631 --- /dev/null +++ b/dashboard/src/api/migrations/0007_opnfvapiconfig_opnfv_config.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1 on 2019-05-01 18:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0010_auto_20190430_1405'), + ('api', '0006_auto_20190313_1729'), + ] + + operations = [ + migrations.AddField( + model_name='opnfvapiconfig', + name='opnfv_config', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.OPNFVConfig'), + ), + ] diff --git a/dashboard/src/api/migrations/0008_auto_20190419_1414.py b/dashboard/src/api/migrations/0008_auto_20190419_1414.py new file mode 100644 index 0000000..03c3865 --- /dev/null +++ b/dashboard/src/api/migrations/0008_auto_20190419_1414.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1 on 2019-04-19 14:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('resource_inventory', '0009_auto_20190315_1757'), + ('api', '0007_auto_20190417_1511'), + ] + + operations = [ + migrations.CreateModel( + name='BridgeConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('interfaces', models.ManyToManyField(to='resource_inventory.Interface')), + ('opnfv_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.OPNFVConfig')), + ], + ), + migrations.AddField( + model_name='opnfvapiconfig', + name='bridge_config', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.BridgeConfig'), + ), + ] diff --git a/dashboard/src/api/migrations/0009_merge_20190508_1317.py b/dashboard/src/api/migrations/0009_merge_20190508_1317.py new file mode 100644 index 0000000..1a34380 --- /dev/null +++ b/dashboard/src/api/migrations/0009_merge_20190508_1317.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1 on 2019-05-08 13:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0008_auto_20190419_1414'), + ('api', '0007_opnfvapiconfig_opnfv_config'), + ] + + operations = [ + ] diff --git a/dashboard/src/api/models.py b/dashboard/src/api/models.py index b6bd79f..1f708ae 100644 --- a/dashboard/src/api/models.py +++ b/dashboard/src/api/models.py @@ -11,6 +11,8 @@ from django.contrib.auth.models import User from django.db import models from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404 +from django.urls import reverse import json import uuid @@ -21,8 +23,13 @@ from resource_inventory.models import ( HostProfile, Host, Image, - Interface + Interface, + HostOPNFVConfig, + RemoteInfo, + OPNFVConfig ) +from resource_inventory.idf_templater import IDFTemplater +from resource_inventory.pdf_templater import PDFTemplater class JobStatus(object): @@ -42,7 +49,7 @@ class LabManagerTracker(object): """ try: lab = Lab.objects.get(name=lab_name) - except: + except Exception: raise PermissionDenied("Lab not found") if lab.api_token == token: return LabManager(lab) @@ -60,6 +67,47 @@ class LabManager(object): def __init__(self, lab): self.lab = lab + def update_host_remote_info(self, data, host_id): + host = get_object_or_404(Host, labid=host_id, lab=self.lab) + 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 = host.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() + host.remote_management = remote_info + host.save() + booking = Booking.objects.get(resource=host.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 @@ -88,6 +136,22 @@ class LabManager(object): inventory['host_types'] = self.serialize_host_profiles(profiles) return inventory + def get_host(self, hostname): + host = get_object_or_404(Host, labid=hostname, lab=self.lab) + return { + "booked": host.booked, + "working": host.working, + "type": host.profile.name + } + + def update_host(self, hostname, data): + host = get_object_or_404(Host, labid=hostname, lab=self.lab) + if "working" in data: + working = data['working'] == "true" + host.working = working + host.save() + return self.get_host(hostname) + def get_status(self): return {"status": self.lab.status} @@ -214,6 +278,10 @@ class Job(models.Model): if 'network' not in d: d['network'] = {} d['network'][relation.task_id] = relation.config.to_dict() + for relation in SnapshotRelation.objects.filter(job=self): + if 'snapshot' not in d: + d['snapshot'] = {} + d['snapshot'][relation.task_id] = relation.config.to_dict() j['payload'] = d @@ -221,7 +289,13 @@ class Job(models.Model): def get_tasklist(self, status="all"): tasklist = [] - clist = [HostHardwareRelation, AccessRelation, HostNetworkRelation, SoftwareRelation] + clist = [ + HostHardwareRelation, + AccessRelation, + HostNetworkRelation, + SoftwareRelation, + SnapshotRelation + ] if status == "all": for cls in clist: tasklist += list(cls.objects.filter(job=self)) @@ -261,6 +335,10 @@ class Job(models.Model): if 'network' not in d: d['network'] = {} d['network'][relation.task_id] = relation.config.get_delta() + for relation in SnapshotRelation.objects.filter(job=self).filter(status=status): + if 'snapshot' not in d: + d['snapshot'] = {} + d['snapshot'][relation.task_id] = relation.config.get_delta() j['payload'] = d return j @@ -283,25 +361,71 @@ class TaskConfig(models.Model): self.delta = '{}' +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 = self.interfaces.first().host.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 OpnfvApiConfig(models.Model): - installer = models.CharField(max_length=100) - scenario = models.CharField(max_length=100) + installer = models.CharField(max_length=200) + scenario = models.CharField(max_length=300) roles = models.ManyToManyField(Host) + # 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 self.roles.all(): - d['roles'].append({host.labid: host.config.opnfvRole.name}) + 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 @@ -320,6 +444,16 @@ class OpnfvApiConfig(models.Model): 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) @@ -343,14 +477,17 @@ class AccessConfig(TaskConfig): user = models.ForeignKey(User, on_delete=models.CASCADE) revoke = models.BooleanField(default=False) context = models.TextField(default="") - delta = models.TextField() + delta = models.TextField(default="{}") def to_dict(self): d = {} d['access_type'] = self.access_type d['user'] = self.user.id d['revoke'] = self.revoke - d['context'] = json.loads(self.context) + try: + d['context'] = json.loads(self.context) + except Exception: + pass return d def get_delta(self): @@ -531,8 +668,64 @@ class NetworkConfig(TaskConfig): self.delta = json.dumps(d) +class SnapshotConfig(TaskConfig): + + host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING) + image = models.IntegerField(null=True) + 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): + if not self.delta: + self.delta = self.to_json() + self.save() + + d = json.loads(self.delta) + 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 get_task(task_id): - for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation]: + for taskclass in [AccessRelation, SoftwareRelation, HostHardwareRelation, HostNetworkRelation, SnapshotRelation]: try: ret = taskclass.objects.get(task_id=task_id) return ret @@ -614,15 +807,72 @@ class HostNetworkRelation(TaskRelation): return super(self.__class__, self).delete(*args, **kwargs) +class SnapshotRelation(TaskRelation): + snapshot = models.ForeignKey(Image, on_delete=models.CASCADE) + config = models.OneToOneField(SnapshotConfig, on_delete=models.CASCADE) + + 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 JobFactory(object): @classmethod + def reimageHost(cls, new_image, booking, host): + """ + This method will make all necessary changes to make a lab + reimage a host. + """ + job = Job.objects.get(booking=booking) + # make hardware task new + hardware_relation = HostHardwareRelation.objects.get(host=host, job=job) + hardware_relation.config.set_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(host=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 makeCompleteJob(cls, booking): hosts = Host.objects.filter(bundle=booking.resource) job = None try: job = Job.objects.get(booking=booking) - except: + except Exception: job = Job.objects.create(status=JobStatus.NEW, booking=booking) cls.makeHardwareConfigs( hosts=hosts, @@ -633,7 +883,7 @@ class JobFactory(object): job=job ) cls.makeSoftware( - hosts=hosts, + booking=booking, job=job ) all_users = list(booking.collaborators.all()) @@ -665,7 +915,7 @@ class JobFactory(object): hardware_config = None try: hardware_config = HardwareConfig.objects.get(relation__host=host) - except: + except Exception: hardware_config = HardwareConfig() relation = HostHardwareRelation() @@ -691,12 +941,12 @@ class JobFactory(object): config = AccessConfig() config.access_type = access_type config.user = user - if context: - config.set_context(context) 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) @@ -708,7 +958,7 @@ class JobFactory(object): network_config = None try: network_config = NetworkConfig.objects.get(relation__host=host) - except: + except Exception: network_config = NetworkConfig.objects.create() relation = HostNetworkRelation() @@ -724,28 +974,42 @@ class JobFactory(object): 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 - + def make_bridge_config(cls, booking): + if booking.resource.hosts.count() < 2: + return None try: - host = None - if len(hosts) > 0: - host = hosts[0] - opnfv_config = init_config(host) + jumphost_config = HostOPNFVConfig.objects.filter( + role__name__iexact="jumphost" + ) + jumphost = Host.objects.get( + bundle=booking.resource, + config=jumphost_config.host_config + ) + 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 - 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: + @classmethod + def makeSoftware(cls, booking=None, job=Job()): + + 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.hosts.all(): + 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 diff --git a/dashboard/src/api/tests/test_models_unittest.py b/dashboard/src/api/tests/test_models_unittest.py new file mode 100644 index 0000000..e6f97a6 --- /dev/null +++ b/dashboard/src/api/tests/test_models_unittest.py @@ -0,0 +1,269 @@ +############################################################################## +# 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, +) + +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): + """ + distributes 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.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)) + + self.assertEqual(relation.config.power, "on") + self.assertTrue(relation.config.ipmi_create) + # TODO: the rest of hwconf attrs + + self.assertEqual(len(host_set), 0) diff --git a/dashboard/src/api/tests/test_serializers.py b/dashboard/src/api/tests/test_serializers.py deleted file mode 100644 index c1fa5af..0000000 --- a/dashboard/src/api/tests/test_serializers.py +++ /dev/null @@ -1,229 +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 booking.models import Booking -from account.models import Lab -from api.serializers.booking_serializer import BookingField -from datetime import timedelta -from django.utils import timezone -from django.contrib.auth.models import Permission, User -from resource_inventory.models import ( - Image, - OPNFVRole, - HostConfiguration, - HostProfile, - InterfaceProfile, - DiskProfile, - CpuProfile, - RamProfile, - GenericResourceBundle, - GenericResource, - GenericHost, - Host, - Vlan, - Interface, - ConfigBundle, - ResourceBundle -) - - -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.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() - - 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 50cc6ac..7a48425 100644 --- a/dashboard/src/api/urls.py +++ b/dashboard/src/api/urls.py @@ -39,6 +39,10 @@ from api.views import ( new_jobs, current_jobs, done_jobs, + update_host_bmc, + lab_host, + get_pdf, + get_idf, GenerateTokenView ) @@ -51,6 +55,10 @@ urlpatterns = [ path('labs/<slug:lab_name>/profile', lab_profile), path('labs/<slug:lab_name>/status', lab_status), path('labs/<slug:lab_name>/inventory', lab_inventory), + path('labs/<slug:lab_name>/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/new', new_jobs), diff --git a/dashboard/src/api/views.py b/dashboard/src/api/views.py index c72c85c..fb28958 100644 --- a/dashboard/src/api/views.py +++ b/dashboard/src/api/views.py @@ -13,7 +13,7 @@ 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 django.http.response import JsonResponse, HttpResponse from rest_framework import viewsets from rest_framework.authtoken.models import Token from django.views.decorators.csrf import csrf_exempt @@ -54,6 +54,28 @@ def lab_inventory(request, lab_name=""): 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) + + +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) @@ -62,6 +84,18 @@ def lab_status(request, lab_name=""): return JsonResponse(lab_manager.get_status(), safe=False) +@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) @@ -79,6 +113,8 @@ def specific_task(request, lab_name="", job_id="", task_id=""): 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 = {} @@ -93,6 +129,7 @@ def specific_task(request, lab_name="", job_id="", task_id=""): 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) |