aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/account/views.py10
-rw-r--r--src/api/migrations/0011_auto_20200218_1536.py29
-rw-r--r--src/api/migrations/0012_manual_20200218_1536.py22
-rw-r--r--src/api/migrations/0013_manual_20200218_1536.py29
-rw-r--r--src/api/migrations/0014_manual_20200220.py18
-rw-r--r--src/api/models.py185
-rw-r--r--src/api/serializers/booking_serializer.py4
-rw-r--r--src/booking/forms.py2
-rw-r--r--src/booking/migrations/0007_remove_booking_config_bundle.py17
-rw-r--r--src/booking/models.py4
-rw-r--r--src/booking/quick_deployer.py284
-rw-r--r--src/booking/views.py12
-rw-r--r--src/dashboard/testing_utils.py209
-rw-r--r--src/dashboard/tests/test_views.py30
-rw-r--r--src/dashboard/utils.py42
-rw-r--r--src/dashboard/views.py4
-rw-r--r--src/resource_inventory/admin.py49
-rw-r--r--src/resource_inventory/migrations/0012_auto_20200103_1850.py37
-rw-r--r--src/resource_inventory/migrations/0012_manual_20200218_1536.py25
-rw-r--r--src/resource_inventory/migrations/0013_auto_20200218_1536.py404
-rw-r--r--src/resource_inventory/models.py468
-rw-r--r--src/resource_inventory/pdf_templater.py6
-rw-r--r--src/resource_inventory/resource_manager.py126
-rw-r--r--src/resource_inventory/views.py6
-rw-r--r--src/workflow/booking_workflow.py7
-rw-r--r--src/workflow/forms.py37
-rw-r--r--src/workflow/models.py6
-rw-r--r--src/workflow/opnfv_workflow.py4
-rw-r--r--src/workflow/resource_bundle_workflow.py30
-rw-r--r--src/workflow/snapshot_workflow.py4
-rw-r--r--src/workflow/sw_bundle_workflow.py12
-rw-r--r--src/workflow/workflow_manager.py13
32 files changed, 1271 insertions, 864 deletions
diff --git a/src/account/views.py b/src/account/views.py
index ccc4c8d..a8bb02b 100644
--- a/src/account/views.py
+++ b/src/account/views.py
@@ -33,7 +33,7 @@ from account.forms import AccountSettingsForm
from account.jira_util import SignatureMethod_RSA_SHA1
from account.models import UserProfile
from booking.models import Booking
-from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image
+from resource_inventory.models import ResourceTemplate, Image
@method_decorator(login_required, name='dispatch')
@@ -177,7 +177,7 @@ def account_resource_view(request):
if not request.user.is_authenticated:
return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
template = "account/resource_list.html"
- resources = GenericResourceBundle.objects.filter(
+ resources = ResourceTemplate.objects.filter(
owner=request.user).prefetch_related("configbundle_set")
mapping = {}
resource_list = []
@@ -218,7 +218,7 @@ def account_configuration_view(request):
if not request.user.is_authenticated:
return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
template = "account/configuration_list.html"
- configs = list(ConfigBundle.objects.filter(owner=request.user))
+ configs = list(ResourceTemplate.objects.filter(owner=request.user))
context = {"title": "Configuration List", "configurations": configs}
return render(request, template, context=context)
@@ -245,7 +245,7 @@ def account_images_view(request):
def resource_delete_view(request, resource_id=None):
if not request.user.is_authenticated:
return HttpResponse('no') # 403?
- grb = get_object_or_404(GenericResourceBundle, pk=resource_id)
+ grb = get_object_or_404(ResourceTemplate, pk=resource_id)
if not request.user.id == grb.owner.id:
return HttpResponse('no') # 403?
if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
@@ -257,7 +257,7 @@ def resource_delete_view(request, resource_id=None):
def configuration_delete_view(request, config_id=None):
if not request.user.is_authenticated:
return HttpResponse('no') # 403?
- config = get_object_or_404(ConfigBundle, pk=config_id)
+ config = get_object_or_404(ResourceTemplate, pk=config_id)
if not request.user.id == config.owner.id:
return HttpResponse('no') # 403?
if Booking.objects.filter(config_bundle=config, end__gt=timezone.now()).exists():
diff --git a/src/api/migrations/0011_auto_20200218_1536.py b/src/api/migrations/0011_auto_20200218_1536.py
new file mode 100644
index 0000000..0fd7029
--- /dev/null
+++ b/src/api/migrations/0011_auto_20200218_1536.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0010_auto_20191219_2004'),
+ # ('resource_inventory', '0013_auto_20200218_1536')
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='hosthardwarerelation',
+ name='resource_id',
+ field=models.CharField(default='default_id', max_length=200),
+ ),
+ migrations.AddField(
+ model_name='hostnetworkrelation',
+ name='resource_id',
+ field=models.CharField(default='default_id', max_length=200),
+ ),
+ migrations.AddField(
+ model_name='snapshotconfig',
+ name='resource_id',
+ field=models.CharField(default='default_id', max_length=200),
+ ),
+ ]
diff --git a/src/api/migrations/0012_manual_20200218_1536.py b/src/api/migrations/0012_manual_20200218_1536.py
new file mode 100644
index 0000000..55befbd
--- /dev/null
+++ b/src/api/migrations/0012_manual_20200218_1536.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.db import migrations
+
+
+def set_resource_id(apps, schema_editor):
+ for cls in ["HostHardwareRelation", "HostNetworkRelation", "SnapshotConfig"]:
+ model = apps.get_model('api', cls)
+ for m in model.objects.all():
+ m.resource_id = m.host.labid
+ m.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0011_auto_20200218_1536'),
+ ]
+
+ operations = [
+ migrations.RunPython(set_resource_id),
+ ]
diff --git a/src/api/migrations/0013_manual_20200218_1536.py b/src/api/migrations/0013_manual_20200218_1536.py
new file mode 100644
index 0000000..0b76e84
--- /dev/null
+++ b/src/api/migrations/0013_manual_20200218_1536.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0012_manual_20200218_1536'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='hosthardwarerelation',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='hostnetworkrelation',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='snapshotconfig',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='opnfvapiconfig',
+ name='roles',
+ ),
+ ]
diff --git a/src/api/migrations/0014_manual_20200220.py b/src/api/migrations/0014_manual_20200220.py
new file mode 100644
index 0000000..2e2cd58
--- /dev/null
+++ b/src/api/migrations/0014_manual_20200220.py
@@ -0,0 +1,18 @@
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0013_manual_20200218_1536'),
+ ('resource_inventory', '0013_auto_20200218_1536')
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='opnfvapiconfig',
+ name='roles',
+ field=models.ManyToManyField(to='resource_inventory.ResourceOPNFVConfig'),
+ ),
+ ]
diff --git a/src/api/models.py b/src/api/models.py
index 1e5a2da..de73a7a 100644
--- a/src/api/models.py
+++ b/src/api/models.py
@@ -10,8 +10,9 @@
from django.contrib.auth.models import User
from django.db import models
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
from django.shortcuts import get_object_or_404
+from django.http import HttpResponseNotFound
from django.urls import reverse
from django.utils import timezone
@@ -21,18 +22,19 @@ import uuid
from booking.models import Booking
from resource_inventory.models import (
Lab,
- HostProfile,
- Host,
+ ResourceProfile,
Image,
Interface,
- HostOPNFVConfig,
+ ResourceOPNFVConfig,
RemoteInfo,
OPNFVConfig,
- ConfigState
+ ConfigState,
+ ResourceQuery
)
from resource_inventory.idf_templater import IDFTemplater
from resource_inventory.pdf_templater import PDFTemplater
from account.models import Downtime
+from dashboard.utils import AbstractModelQuery
class JobStatus(object):
@@ -115,8 +117,11 @@ class LabManager(object):
)
return self.get_downtime_json()
- def update_host_remote_info(self, data, host_id):
- host = get_object_or_404(Host, labid=host_id, lab=self.lab)
+ 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']
@@ -127,7 +132,7 @@ class LabManager(object):
info['versions'] = json.dumps(data['versions'])
except Exception as e:
return {"error": "invalid arguement: " + str(e)}
- remote_info = host.remote_management
+ remote_info = resource.remote_management
if "default" in remote_info.mac_address:
remote_info = RemoteInfo()
remote_info.address = info['address']
@@ -137,9 +142,9 @@ class LabManager(object):
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)
+ resource.remote_management = remote_info
+ resource.save()
+ booking = Booking.objects.get(resource=resource.bundle)
self.update_xdf(booking)
return {"status": "success"}
@@ -163,41 +168,42 @@ class LabManager(object):
"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
- }
- )
+ prof['host_count'] = [{
+ "type": profile.name,
+ "count": len(profile.get_resources(lab=self.lab))}
+ for profile in ResourceProfile.objects.filter(labs=self.lab)]
return prof
def get_inventory(self):
inventory = {}
- hosts = Host.objects.filter(lab=self.lab)
+ resources = ResourceQuery.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)
+ 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):
- host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+ 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": host.booked,
- "working": host.working,
- "type": host.profile.name
+ "booked": resource.booked,
+ "working": resource.working,
+ "type": resource.profile.name
}
def update_host(self, hostname, data):
- host = get_object_or_404(Host, labid=hostname, lab=self.lab)
+ 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"
- host.working = working
- host.save()
+ resource.working = working
+ resource.save()
return self.get_host(hostname)
def get_status(self):
@@ -237,20 +243,22 @@ class LabManager(object):
return job_ser
- def serialize_hosts(self, hosts):
+ def serialize_resources(self, resources):
+ # TODO: rewrite for Resource model
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)
+ 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):
@@ -265,7 +273,7 @@ class LabManager(object):
)
return images_ser
- def serialize_host_profiles(self, profiles):
+ def serialize_resource_profiles(self, profiles):
profile_ser = []
for profile in profiles:
p = {}
@@ -323,21 +331,9 @@ class Job(models.Model):
return {"id": self.id, "payload": d}
def get_tasklist(self, status="all"):
- tasklist = []
- clist = [
- HostHardwareRelation,
- AccessRelation,
- HostNetworkRelation,
- SoftwareRelation,
- SnapshotRelation
- ]
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
+ return JobTaskQuery.filter(job=self, status=status)
+ return JobTaskQuery.filter(job=self)
def is_fulfilled(self):
"""
@@ -435,7 +431,7 @@ class OpnfvApiConfig(models.Model):
installer = models.CharField(max_length=200)
scenario = models.CharField(max_length=300)
- roles = models.ManyToManyField(Host)
+ 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)
@@ -632,6 +628,8 @@ class NetworkConfig(TaskConfig):
for interface in self.interfaces.all():
d[hid][interface.mac_address] = []
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
@@ -665,7 +663,7 @@ class NetworkConfig(TaskConfig):
class SnapshotConfig(TaskConfig):
- host = models.ForeignKey(Host, null=True, on_delete=models.DO_NOTHING)
+ resource_id = models.CharField(max_length=200, default="default_id")
image = models.IntegerField(null=True)
dashboard_id = models.IntegerField()
delta = models.TextField(default="{}")
@@ -718,6 +716,11 @@ class SnapshotConfig(TaskConfig):
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]:
@@ -787,7 +790,7 @@ class SoftwareRelation(TaskRelation):
class HostHardwareRelation(TaskRelation):
- host = models.ForeignKey(Host, on_delete=models.CASCADE)
+ resource_id = models.CharField(max_length=200, default="default_id")
config = models.OneToOneField(HardwareConfig, on_delete=models.CASCADE)
job_key = "hardware"
@@ -801,9 +804,14 @@ class HostHardwareRelation(TaskRelation):
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)
+
class HostNetworkRelation(TaskRelation):
- host = models.ForeignKey(Host, on_delete=models.CASCADE)
+ resource_id = models.CharField(max_length=200, default="default_id")
config = models.OneToOneField(NetworkConfig, on_delete=models.CASCADE)
job_key = "network"
@@ -814,6 +822,11 @@ class HostNetworkRelation(TaskRelation):
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)
+
class SnapshotRelation(TaskRelation):
snapshot = models.ForeignKey(Image, on_delete=models.CASCADE)
@@ -876,18 +889,18 @@ class JobFactory(object):
@classmethod
def makeCompleteJob(cls, booking):
"""Create everything that is needed to fulfill the given booking."""
- hosts = Host.objects.filter(bundle=booking.resource)
+ 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(
- hosts=hosts,
+ resources=resources,
job=job
)
cls.makeNetworkConfigs(
- hosts=hosts,
+ resources=resources,
job=job
)
cls.makeSoftware(
@@ -911,29 +924,29 @@ class JobFactory(object):
job=job,
context={
"key": user.userprofile.ssh_public_key.open().read().decode(encoding="UTF-8"),
- "hosts": [host.labid for host in hosts]
+ "hosts": [r.labid for r in resources]
}
)
except Exception:
continue
@classmethod
- def makeHardwareConfigs(cls, hosts=[], job=Job()):
+ def makeHardwareConfigs(cls, resources=[], job=Job()):
"""
Create and save HardwareConfig.
Helper function to create the tasks related to
configuring the hardware
"""
- for host in hosts:
+ for res in resources:
hardware_config = None
try:
- hardware_config = HardwareConfig.objects.get(relation__host=host)
+ hardware_config = HardwareConfig.objects.get(relation__host=res)
except Exception:
hardware_config = HardwareConfig()
relation = HostHardwareRelation()
- relation.host = host
+ relation.resource_id = res.labid
relation.job = job
relation.config = hardware_config
relation.config.save()
@@ -969,29 +982,30 @@ class JobFactory(object):
config.save()
@classmethod
- def makeNetworkConfigs(cls, hosts=[], job=Job()):
+ def makeNetworkConfigs(cls, resources=[], job=Job()):
"""
Create and save NetworkConfig.
Helper function to create the tasks related to
configuring the networking
"""
- for host in hosts:
+ for res in resources:
network_config = None
try:
- network_config = NetworkConfig.objects.get(relation__host=host)
+ network_config = NetworkConfig.objects.get(relation__host=res)
except Exception:
network_config = NetworkConfig.objects.create()
relation = HostNetworkRelation()
- relation.host = host
+ relation.resource_id = res.labid
relation.job = job
network_config.save()
relation.config = network_config
relation.save()
network_config.clear_delta()
- for interface in host.interfaces.all():
+ # TODO: use get_interfaces() on resource
+ for interface in res.interfaces.all():
network_config.add_interface(interface)
network_config.save()
@@ -1000,13 +1014,13 @@ class JobFactory(object):
if booking.resource.hosts.count() < 2:
return None
try:
- jumphost_config = HostOPNFVConfig.objects.filter(
+ jumphost_config = ResourceOPNFVConfig.objects.filter(
role__name__iexact="jumphost"
)
- jumphost = Host.objects.get(
+ jumphost = ResourceQuery.filter(
bundle=booking.resource,
- config=jumphost_config.host_config
- )
+ config=jumphost_config.resource_config
+ )[0]
except Exception:
return None
br_config = BridgeConfig.objects.create(opnfv_config=booking.opnfv_config)
@@ -1040,3 +1054,16 @@ class JobFactory(object):
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
+]
+
+
+class JobTaskQuery(AbstractModelQuery):
+ model_list = JOB_TASK_CLASSLIST
diff --git a/src/api/serializers/booking_serializer.py b/src/api/serializers/booking_serializer.py
index 46a2348..993eb22 100644
--- a/src/api/serializers/booking_serializer.py
+++ b/src/api/serializers/booking_serializer.py
@@ -11,7 +11,7 @@
from rest_framework import serializers
from resource_inventory.models import (
- HostConfiguration,
+ ResourceConfiguration,
CpuProfile,
DiskProfile,
InterfaceProfile,
@@ -35,7 +35,7 @@ class BookingField(serializers.Field):
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)
+ 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
diff --git a/src/booking/forms.py b/src/booking/forms.py
index 9b4db86..b9c9231 100644
--- a/src/booking/forms.py
+++ b/src/booking/forms.py
@@ -47,7 +47,7 @@ class QuickBookingForm(forms.Form):
**get_user_field_opts()
)
- attrs = FormUtils.getLabData(0)
+ attrs = FormUtils.getLabData()
self.fields['filter_field'] = MultipleSelectFilterField(widget=MultipleSelectFilterWidget(**attrs))
self.fields['length'] = forms.IntegerField(
widget=NumberInput(
diff --git a/src/booking/migrations/0007_remove_booking_config_bundle.py b/src/booking/migrations/0007_remove_booking_config_bundle.py
new file mode 100644
index 0000000..dcd2e1c
--- /dev/null
+++ b/src/booking/migrations/0007_remove_booking_config_bundle.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('booking', '0006_booking_opnfv_config'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='booking',
+ name='config_bundle',
+ ),
+ ]
diff --git a/src/booking/models.py b/src/booking/models.py
index 8f2446f..cf8bf1d 100644
--- a/src/booking/models.py
+++ b/src/booking/models.py
@@ -9,7 +9,7 @@
##############################################################################
-from resource_inventory.models import ResourceBundle, ConfigBundle, OPNFVConfig
+from resource_inventory.models import ResourceBundle, OPNFVConfig
from account.models import Lab
from django.contrib.auth.models import User
from django.db import models
@@ -33,8 +33,6 @@ class Booking(models.Model):
ext_count = models.IntegerField(default=2)
# the hardware that the user has booked
resource = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True)
- # configuration for the above hardware
- config_bundle = models.ForeignKey(ConfigBundle, on_delete=models.SET_NULL, null=True)
opnfv_config = models.ForeignKey(OPNFVConfig, on_delete=models.SET_NULL, null=True)
project = models.CharField(max_length=100, default="", blank=True, null=True)
lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL)
diff --git a/src/booking/quick_deployer.py b/src/booking/quick_deployer.py
index 743cdcf..917f578 100644
--- a/src/booking/quick_deployer.py
+++ b/src/booking/quick_deployer.py
@@ -9,208 +9,71 @@
import json
-import uuid
-import re
from django.db.models import Q
from datetime import timedelta
from django.utils import timezone
+from django.core.exceptions import ValidationError
from account.models import Lab
from resource_inventory.models import (
+ ResourceTemplate,
Installer,
Image,
- GenericResourceBundle,
- ConfigBundle,
- Host,
- HostProfile,
- HostConfiguration,
- GenericResource,
- GenericHost,
- GenericInterface,
OPNFVRole,
OPNFVConfig,
- Network,
- NetworkConnection,
- NetworkRole,
- HostOPNFVConfig,
+ ResourceOPNFVConfig,
)
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 (
- InvalidHostnameException,
- ResourceAvailabilityException,
- ModelValidationException,
- BookingLengthException
-)
+from dashboard.exceptions import BookingLengthException
from api.models import JobFactory
-# model validity exceptions
-class IncompatibleInstallerForOS(Exception):
- pass
-
-
-class IncompatibleScenarioForInstaller(Exception):
- pass
-
-
-class IncompatibleImageForHost(Exception):
- pass
-
-
-class ImageOwnershipInvalid(Exception):
- pass
-
-
-class ImageNotAvailableAtLab(Exception):
- pass
-
-
-class LabDNE(Exception):
- pass
-
-
-class HostProfileDNE(Exception):
- pass
-
-
-class HostNotAvailable(Exception):
- pass
-
-
-class NoLabSelectedError(Exception):
- pass
-
-
-class OPNFVRoleDNE(Exception):
- pass
-
-
-class NoRemainingPublicNetwork(Exception):
- pass
-
-
-class BookingPermissionException(Exception):
- pass
-
-
-def parse_host_field(host_json):
+def parse_resource_field(resource_json):
"""
Parse the json from the frontend.
- returns a reference to the selected Lab and HostProfile objects
+ returns a reference to the selected Lab and ResourceTemplate objects
"""
- lab, profile = (None, None)
- lab_dict = host_json['lab']
+ 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'])
- host_dict = host_json['host']
- for host_info in host_dict.values():
- if host_info['selected']:
- profile = HostProfile.objects.get(pk=host_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 NoLabSelectedError("No lab was selected")
- if profile is None:
- raise HostProfileDNE("No Host was selected")
+ raise ValidationError("No lab was selected")
+ if template is None:
+ raise ValidationError("No Host was selected")
- return lab, profile
+ return lab, template
-def check_available_matching_host(lab, hostprofile):
+def update_template(template, image, lab, hostname):
"""
- Check the resources are available.
+ Update and copy a resource template to the user's profile.
- Returns true if the requested host type is availble,
- Or throws an exception
+ TODO: How, why, should we?
"""
- available_host_types = ResourceManager.getInstance().getAvailableHostTypes(lab)
- if hostprofile not in available_host_types:
- # TODO: handle deleting generic resource in this instance along with grb
- raise HostNotAvailable('Requested host type is not available. Please try again later. Host availability can be viewed in the "Hosts" tab to the left.')
-
- hostset = Host.objects.filter(lab=lab, profile=hostprofile).filter(booked=False).filter(working=True)
- if not hostset.exists():
- raise HostNotAvailable("Couldn't find any matching unbooked hosts")
-
- return True
-
-
-# Functions to create models
-
-def generate_grb(owner, lab, common_id):
- """Create a Generic Resource Bundle."""
- grbundle = GenericResourceBundle(owner=owner)
- grbundle.lab = lab
- grbundle.name = "grbundle for quick booking with uid " + common_id
- grbundle.description = "grbundle created for quick-deploy booking"
- grbundle.save()
-
- return grbundle
-
-
-def generate_gresource(bundle, hostname):
- """Create a Generic Resource."""
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions to it until this point")
- gresource = GenericResource(bundle=bundle, name=hostname)
- gresource.save()
-
- return gresource
-
-
-def generate_ghost(generic_resource, host_profile):
- """Create a Generic Host."""
- ghost = GenericHost()
- ghost.resource = generic_resource
- ghost.profile = host_profile
- ghost.save()
-
- return ghost
-
-
-def generate_config_bundle(owner, common_id, grbundle):
- """Create a Configuration Bundle."""
- cbundle = ConfigBundle()
- cbundle.owner = owner
- cbundle.name = "configbundle for quick booking with uid " + common_id
- cbundle.description = "configbundle created for quick-deploy booking"
- cbundle.bundle = grbundle
- cbundle.save()
-
- return cbundle
-
-
-def generate_opnfvconfig(scenario, installer, config_bundle):
- """Create an OPNFV Configuration."""
- opnfvconfig = OPNFVConfig()
- opnfvconfig.scenario = scenario
- opnfvconfig.installer = installer
- opnfvconfig.bundle = config_bundle
- opnfvconfig.save()
-
- return opnfvconfig
-
+ pass
-def generate_hostconfig(generic_host, image, config_bundle):
- """Create a Host Configuration."""
- hconf = HostConfiguration()
- hconf.host = generic_host
- hconf.image = image
- hconf.bundle = config_bundle
- hconf.is_head_node = True
- hconf.save()
- return hconf
+def generate_opnfvconfig(scenario, installer, template):
+ return OPNFVConfig.objects.create(
+ scenario=scenario,
+ installer=installer,
+ template=template
+ )
def generate_hostopnfv(hostconfig, opnfvconfig):
- """Relate the Host and OPNFV Configs."""
- config = HostOPNFVConfig()
role = None
try:
role = OPNFVRole.objects.get(name="Jumphost")
@@ -219,31 +82,21 @@ def generate_hostopnfv(hostconfig, opnfvconfig):
name="Jumphost",
description="Single server jumphost role"
)
- config.role = role
- config.host_config = hostconfig
- config.opnfv_config = opnfvconfig
- config.save()
- return config
+ return ResourceOPNFVConfig.objects.create(
+ role=role,
+ host_config=hostconfig,
+ opnfv_config=opnfvconfig
+ )
-def generate_resource_bundle(generic_resource_bundle, config_bundle): # warning: requires cleanup
- """Create a Resource Bundle."""
- try:
- resource_manager = ResourceManager.getInstance()
- resource_bundle = resource_manager.convertResourceBundle(generic_resource_bundle, config=config_bundle)
- return resource_bundle
- except ResourceAvailabilityException:
- raise ResourceAvailabilityException("Requested resources not available")
- except ModelValidationException:
- raise ModelValidationException("Encountered error while saving grbundle")
+def generate_resource_bundle(template):
+ resource_manager = ResourceManager.getInstance()
+ resource_bundle = resource_manager.convertResourceBundle(template)
+ return resource_bundle
def check_invariants(request, **kwargs):
- """
- Verify all the contraints on the requested booking.
-
- verifies software compatibility, booking length, etc
- """
+ # TODO: This should really happen in the BookingForm validation methods
installer = kwargs['installer']
image = kwargs['image']
scenario = kwargs['scenario']
@@ -254,33 +107,19 @@ def check_invariants(request, **kwargs):
if installer in image.os.sup_installers.all():
# if installer not here, we can omit that and not check for scenario
if not scenario:
- raise IncompatibleScenarioForInstaller("An OPNFV Installer needs a scenario to be chosen to work properly")
+ raise ValidationError("An OPNFV Installer needs a scenario to be chosen to work properly")
if scenario not in installer.sup_scenarios.all():
- raise IncompatibleScenarioForInstaller("The chosen installer does not support the chosen scenario")
+ raise ValidationError("The chosen installer does not support the chosen scenario")
if image.from_lab != lab:
- raise ImageNotAvailableAtLab("The chosen image is not available at the chosen hosting lab")
+ raise ValidationError("The chosen image is not available at the chosen hosting lab")
if image.host_type != host_profile:
- raise IncompatibleImageForHost("The chosen image is not available for the chosen host type")
+ raise ValidationError("The chosen image is not available for the chosen host type")
if not image.public and image.owner != request.user:
- raise ImageOwnershipInvalid("You are not the owner of the chosen private image")
+ 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 configure_networking(grb, config):
- # create network
- net = Network.objects.create(name="public", bundle=grb, is_public=True)
- # connect network to generic host
- grb.getResources()[0].generic_interfaces.first().connections.add(
- NetworkConnection.objects.create(network=net, vlan_is_tagged=False)
- )
- # asign network role
- role = NetworkRole.objects.create(name="public", network=net)
- opnfv_config = config.opnfv_config.first()
- if opnfv_config:
- opnfv_config.networks.add(role)
-
-
def create_from_form(form, request):
"""
Create a Booking from the user's form.
@@ -288,9 +127,7 @@ def create_from_form(form, request):
Large, nasty method to create a booking or return a useful error
based on the form from the frontend
"""
- quick_booking_id = str(uuid.uuid4())
-
- host_field = form.cleaned_data['filter_field']
+ resource_field = form.cleaned_data['filter_field']
purpose_field = form.cleaned_data['purpose']
project_field = form.cleaned_data['project']
users_field = form.cleaned_data['users']
@@ -301,39 +138,30 @@ def create_from_form(form, request):
scenario = form.cleaned_data['scenario']
installer = form.cleaned_data['installer']
- lab, host_profile = parse_host_field(host_field)
+ lab, resource_template = parse_resource_field(resource_field)
data = form.cleaned_data
data['lab'] = lab
- data['host_profile'] = host_profile
+ data['resource_template'] = resource_template
check_invariants(request, **data)
# check booking privileges
+ # TODO: use the canonical booking_allowed method because now template might have multiple
+ # machines
if Booking.objects.filter(owner=request.user, end__gt=timezone.now()).count() >= 3 and not request.user.userprofile.booking_privledge:
- raise BookingPermissionException("You do not have permission to have more than 3 bookings at a time.")
+ raise PermissionError("You do not have permission to have more than 3 bookings at a time.")
- check_available_matching_host(lab, host_profile) # requires cleanup if failure after this point
+ ResourceManager.getInstance().templateIsReservable(resource_template)
- grbundle = generate_grb(request.user, lab, quick_booking_id)
- gresource = generate_gresource(grbundle, hostname)
- ghost = generate_ghost(gresource, host_profile)
- cbundle = generate_config_bundle(request.user, quick_booking_id, grbundle)
- hconf = generate_hostconfig(ghost, image, cbundle)
+ hconf = update_template(resource_template, image, lab, hostname)
# if no installer provided, just create blank host
opnfv_config = None
if installer:
- opnfv_config = generate_opnfvconfig(scenario, installer, cbundle)
+ opnfv_config = generate_opnfvconfig(scenario, installer, resource_template)
generate_hostopnfv(hconf, opnfv_config)
- # construct generic interfaces
- for interface_profile in host_profile.interfaceprofile.all():
- generic_interface = GenericInterface.objects.create(profile=interface_profile, host=ghost)
- generic_interface.save()
-
- configure_networking(grbundle, cbundle)
-
# generate resource bundle
- resource_bundle = generate_resource_bundle(grbundle, cbundle)
+ resource_bundle = generate_resource_bundle(resource_template)
# generate booking
booking = Booking.objects.create(
@@ -344,7 +172,6 @@ def create_from_form(form, request):
start=timezone.now(),
end=timezone.now() + timedelta(days=int(length)),
resource=resource_bundle,
- config_bundle=cbundle,
opnfv_config=opnfv_config
)
booking.pdf = PDFTemplater.makePDF(booking)
@@ -384,10 +211,11 @@ def drop_filter(user):
images = Image.objects.filter(Q(public=True) | Q(owner=user))
image_filter = {}
for image in images:
- image_filter[image.id] = {}
- image_filter[image.id]['lab'] = 'lab_' + str(image.from_lab.lab_user.id)
- image_filter[image.id]['host_profile'] = 'host_' + str(image.host_type.id)
- image_filter[image.id]['name'] = image.name
+ image_filter[image.id] = {
+ 'lab': 'lab_' + str(image.from_lab.lab_user.id),
+ 'host_profile': 'host_' + str(image.host_type.id),
+ 'name': image.name
+ }
return {'installer_filter': json.dumps(installer_filter),
'scenario_filter': json.dumps(scenario_filter),
diff --git a/src/booking/views.py b/src/booking/views.py
index 8e25952..daaf026 100644
--- a/src/booking/views.py
+++ b/src/booking/views.py
@@ -18,7 +18,7 @@ from django.shortcuts import redirect, render
from django.db.models import Q
from django.urls import reverse
-from resource_inventory.models import ResourceBundle, HostProfile, Image, Host
+from resource_inventory.models import ResourceBundle, ResourceProfile, Image, ResourceQuery
from resource_inventory.resource_manager import ResourceManager
from account.models import Lab, Downtime
from booking.models import Booking
@@ -42,11 +42,11 @@ def quick_create(request):
context = {}
r_manager = ResourceManager.getInstance()
- profiles = {}
+ templates = {}
for lab in Lab.objects.all():
- profiles[str(lab)] = r_manager.getAvailableHostTypes(lab)
+ templates[str(lab)] = r_manager.getAvailableResourceTemplates(lab, request.user)
- context['lab_profile_map'] = profiles
+ context['lab_profile_map'] = templates
context['form'] = QuickBookingForm(default_user=request.user.username, user=request.user)
@@ -130,7 +130,7 @@ class ResourceBookingsJSON(View):
def build_image_mapping(lab, user):
mapping = {}
- for profile in HostProfile.objects.filter(labs=lab):
+ for profile in ResourceProfile.objects.filter(labs=lab):
images = Image.objects.filter(
from_lab=lab,
host_type=profile
@@ -178,7 +178,7 @@ def booking_modify_image(request, booking_id):
if timezone.now() > booking.end:
return HttpResponse("unauthorized")
new_image = Image.objects.get(id=form.cleaned_data['image_id'])
- host = Host.objects.get(id=form.cleaned_data['host_id'])
+ host = ResourceQuery.get(labid=form.cleaned_data['host_id'])
host.config.image = new_image
host.config.save()
JobFactory.reimageHost(new_image, booking, host)
diff --git a/src/dashboard/testing_utils.py b/src/dashboard/testing_utils.py
index 0f52daa..506e998 100644
--- a/src/dashboard/testing_utils.py
+++ b/src/dashboard/testing_utils.py
@@ -12,16 +12,17 @@ from django.core.files.base import ContentFile
from django.utils import timezone
import json
-import re
from datetime import timedelta
-from dashboard.exceptions import InvalidHostnameException
from booking.models import Booking
from account.models import UserProfile, Lab, LabStatus, VlanManager, PublicNetwork
from resource_inventory.models import (
- Host,
- HostProfile,
+ ResourceTemplate,
+ ResourceProfile,
+ ResourceConfiguration,
InterfaceProfile,
+ InterfaceConfiguration,
+ Server,
DiskProfile,
CpuProfile,
Opsys,
@@ -31,15 +32,6 @@ from resource_inventory.models import (
OPNFVRole,
RamProfile,
Network,
- GenericResourceBundle,
- GenericResource,
- GenericHost,
- ConfigBundle,
- GenericInterface,
- HostConfiguration,
- OPNFVConfig,
- NetworkConnection,
- HostOPNFVConfig
)
from resource_inventory.resource_manager import ResourceManager
@@ -79,59 +71,22 @@ def make_booking(owner=None, start=timezone.now(),
project="my_project", collaborators=[],
topology={}, installer=None, scenario=None):
- grb, host_set = make_grb(topology, owner, lab)
- config_bundle, opnfv_bundle = make_config_bundle(grb, owner, topology, host_set, installer, scenario)
- resource = ResourceManager.getInstance().convertResourceBundle(grb, config=config_bundle)
+ 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,
- config_bundle=config_bundle,
start=start,
end=end,
owner=owner,
purpose=purpose,
project=project,
lab=lab,
- opnfv_config=opnfv_bundle
)
-def make_config_bundle(grb, owner, topology={}, host_set={},
- installer=None, scenario=None):
- cb = ConfigBundle.objects.create(
- owner=owner,
- name="config bundle " + str(ConfigBundle.objects.count()),
- description="cb generated by make_config_bundle() method",
- bundle=grb
- )
-
- scen = scenario or Scenario.objects.first() or make_scenario()
- inst = installer or Installer.objects.first() or make_installer([scen])
-
- opnfv_config = OPNFVConfig.objects.create(
- installer=inst,
- scenario=scen,
- bundle=cb
- )
-
- # generate host configurations based on topology and host set
- for hostname, host_info in topology.items():
- host_config = HostConfiguration.objects.create(
- host=host_set[hostname],
- image=host_info["image"],
- bundle=cb,
- is_head_node=host_info['role'].name.lower() == "jumphost"
- )
- HostOPNFVConfig.objects.create(
- role=host_info["role"],
- host_config=host_config,
- opnfv_config=opnfv_config
- )
- return cb, opnfv_config
-
-
def make_network(name, lab, grb, public):
network = Network(name=name, bundle=grb, is_public=public)
if public:
@@ -151,49 +106,33 @@ def make_network(name, lab, grb, public):
return network
-def make_grb(topology, owner, lab):
-
- grb = GenericResourceBundle.objects.create(
- owner=owner,
- lab=lab,
- name="Generic ResourceBundle " + str(GenericResourceBundle.objects.count()),
- description="grb generated by make_grb() method"
- )
-
- networks = {}
- host_set = {}
-
- for hostname, info in topology.items():
- host_profile = info["type"]
+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
- # need to construct host from hostname and type
- generic_host = make_generic_host(grb, host_profile, hostname)
- host_set[hostname] = generic_host
- # set up networks
- nets = info["nets"]
- for interface_index, interface_profile in enumerate(host_profile.interfaceprofile.all()):
- generic_interface = GenericInterface.objects.create(host=generic_host, profile=interface_profile)
- netconfig = nets[interface_index]
- for network_info in netconfig:
- network_name = network_info["name"]
- if network_name not in networks:
- networks[network_name] = make_network(network_name, lab, grb, network_info['public'])
+def make_resource_config(template, profile=None, image=None):
+ if profile is None:
+ profile = make_resource_profile(lab=template.lab)
- generic_interface.connections.add(NetworkConnection.objects.create(
- network=networks[network_name],
- vlan_is_tagged=network_info["tagged"]
- ))
+ if image is None:
+ image = make_image(profile, lab=template.lab)
- return grb, host_set
+ return ResourceConfiguration.objects.create(profile=profile, image=image, template=template)
-def make_generic_host(grb, host_profile, hostname):
- if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})$", hostname):
- raise InvalidHostnameException("Hostname must comply to RFC 952 and all extensions")
- gresource = GenericResource.objects.create(bundle=grb, name=hostname)
+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]
- return GenericHost.objects.create(resource=gresource, profile=host_profile)
+ # not adding any connections here
+ return InterfaceConfiguration.objects.create(profile=iface_profile, resource_config=resource_config)
def make_user(is_superuser=False, username="testuser",
@@ -267,80 +206,48 @@ resource_inventory instantiation section for permanent resources
"""
-def make_complete_host_profile(lab, name="test_hostprofile"):
- host_profile = make_host_profile(lab, name=name)
- make_disk_profile(host_profile, 500, name=name)
- make_cpu_profile(host_profile)
- make_interface_profile(host_profile, name=name)
- make_ram_profile(host_profile)
-
- return host_profile
-
-
-def make_host_profile(lab, host_type=0, name="test hostprofile"):
- host_profile = HostProfile.objects.create(
- host_type=host_type,
+def make_resource_profile(lab, name="test_hostprofile"):
+ resource_profile = ResourceProfile.objects.create(
name=name,
- description='test hostprofile instance'
+ description='test resourceprofile instance'
)
- host_profile.labs.add(lab)
-
- return host_profile
-
-
-def make_ram_profile(host, channels=4, amount=256):
- return RamProfile.objects.create(
- host=host,
- amount=amount,
- channels=channels
+ 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
)
-
-def make_disk_profile(hostprofile, size=0, media_type="SSD",
- name="test diskprofile", rotation=0,
- interface="sata"):
- return DiskProfile.objects.create(
- name=name,
- size=size,
- media_type=media_type,
- host=hostprofile,
- rotation=rotation,
- interface=interface
+ InterfaceProfile.objects.create(
+ host=resource_profile,
+ name="test interface profile",
+ speed=1000,
+ nic_type="pcie"
)
+ return resource_profile
-def make_cpu_profile(hostprofile,
- cores=4,
- architecture="x86_64",
- cpus=4,):
- return CpuProfile.objects.create(
- cores=cores,
- architecture=architecture,
- cpus=cpus,
- host=hostprofile,
- cflags=''
- )
+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()
-def make_interface_profile(hostprofile,
- speed=1000,
- name="test interface profile",
- nic_type="pcie"):
- return InterfaceProfile.objects.create(
- host=hostprofile,
- name=name,
- speed=speed,
- nic_type=nic_type
- )
+ if owner is None:
+ owner = make_user()
+ if os is None:
+ os = make_os()
-def make_image(lab, lab_id, owner, os, host_profile,
- public=True, name="default image", description="default image"):
return Image.objects.create(
from_lab=lab,
lab_id=lab_id,
os=os,
- host_type=host_profile,
+ host_type=resource_profile,
public=public,
name=name,
description=description
@@ -369,10 +276,10 @@ def make_os(installers=None, name="test OS"):
return os
-def make_host(host_profile, lab, labid="test_host", name="test_host",
- booked=False, working=True, config=None, template=None,
- bundle=None, model="Model 1", vendor="ACME"):
- return Host.objects.create(
+def make_server(host_profile, lab, labid="test_host", name="test_host",
+ booked=False, working=True, config=None, template=None,
+ bundle=None, model="Model 1", vendor="ACME"):
+ return Server.objects.create(
lab=lab,
profile=host_profile,
name=name,
diff --git a/src/dashboard/tests/test_views.py b/src/dashboard/tests/test_views.py
new file mode 100644
index 0000000..f2d5490
--- /dev/null
+++ b/src/dashboard/tests/test_views.py
@@ -0,0 +1,30 @@
+##############################################################################
+# 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/utils.py b/src/dashboard/utils.py
new file mode 100644
index 0000000..3d63366
--- /dev/null
+++ b/src/dashboard/utils.py
@@ -0,0 +1,42 @@
+##############################################################################
+# Copyright (c) 2020 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.core.exceptions import ObjectDoesNotExist
+
+
+class AbstractModelQuery():
+ """
+ This is a class made for querying abstract models.
+
+ This class is itself abstract. create subclasses to
+ query your own abstract models.
+ """
+
+ model_list = []
+
+ @classmethod
+ def filter(cls, *args, **kwargs):
+ """
+ Query all concrete model classes.
+
+ Iterates over the model list and returns a list of all
+ matching models from the classes given.
+ Filter queries are given here as normal and are passed into the Django ORM
+ for each concrete model
+ """
+ result = []
+ for model in cls.model_list:
+ result += list(model.objects.filter(*args, **kwargs))
+
+ @classmethod
+ def get(cls, *args, **kwargs):
+ try:
+ return cls.filter(*args, **kwargs)[0]
+ except IndexError:
+ raise ObjectDoesNotExist()
diff --git a/src/dashboard/views.py b/src/dashboard/views.py
index 2f37774..498bd9d 100644
--- a/src/dashboard/views.py
+++ b/src/dashboard/views.py
@@ -15,7 +15,7 @@ from django.shortcuts import render
from account.models import Lab
-from resource_inventory.models import Image, HostProfile
+from resource_inventory.models import Image, ResourceProfile
from workflow.workflow_manager import ManagerTracker
@@ -80,7 +80,7 @@ class LandingView(TemplateView):
hosts = []
- for host_profile in HostProfile.objects.all():
+ for host_profile in ResourceProfile.objects.all():
name = host_profile.name
description = host_profile.description
in_labs = host_profile.labs
diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py
index 7ff510b..13afd99 100644
--- a/src/resource_inventory/admin.py
+++ b/src/resource_inventory/admin.py
@@ -11,16 +11,15 @@
from django.contrib import admin
from resource_inventory.models import (
- HostProfile,
+ ResourceProfile,
InterfaceProfile,
DiskProfile,
CpuProfile,
RamProfile,
- GenericResourceBundle,
- GenericResource,
- GenericHost,
- GenericInterface,
- Host,
+ ResourceTemplate,
+ ResourceConfiguration,
+ InterfaceConfiguration,
+ Server,
Interface,
Network,
Vlan,
@@ -28,26 +27,30 @@ from resource_inventory.models import (
Scenario,
Installer,
Opsys,
- ConfigBundle,
OPNFVConfig,
OPNFVRole,
Image,
- HostConfiguration,
RemoteInfo
)
-profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile]
-
-admin.site.register(profiles)
-
-generics = [GenericResourceBundle, GenericResource, GenericHost, GenericInterface]
-
-admin.site.register(generics)
-
-physical = [Host, Interface, Network, Vlan, ResourceBundle]
-
-admin.site.register(physical)
-
-config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration, RemoteInfo]
-
-admin.site.register(config)
+admin.site.register([
+ ResourceProfile,
+ InterfaceProfile,
+ DiskProfile,
+ CpuProfile,
+ RamProfile,
+ ResourceTemplate,
+ ResourceConfiguration,
+ InterfaceConfiguration,
+ Server,
+ Interface,
+ Network,
+ Vlan,
+ ResourceBundle,
+ Scenario,
+ Installer,
+ Opsys,
+ OPNFVConfig,
+ OPNFVRole,
+ Image,
+ RemoteInfo])
diff --git a/src/resource_inventory/migrations/0012_auto_20200103_1850.py b/src/resource_inventory/migrations/0012_auto_20200103_1850.py
index 2bb203e..569e433 100644
--- a/src/resource_inventory/migrations/0012_auto_20200103_1850.py
+++ b/src/resource_inventory/migrations/0012_auto_20200103_1850.py
@@ -4,30 +4,23 @@ from django.db import migrations, models
import django.db.models.deletion
-def genTempVlanNetwork(apps, editor):
+def pairVlanPhysicalNetworks(apps, editor):
+ PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork")
Vlan = apps.get_model("resource_inventory", "Vlan")
- Network = apps.get_model("resource_inventory", "Network")
- tempVlanNetwork = apps.get_model("resource_inventory", "tempVlanNetwork")
for vlan in Vlan.objects.filter(network__isnull=False):
- tempVlanNetwork.objects.create(network=vlan.network, vlan=vlan)
+ if PhysicalNetwork.objects.filter(id=vlan.network.id).exists():
+ continue
+ PhysicalNetwork.objects.create(id=vlan.network.id, vlan_id=vlan.vlan_id, generic_network=vlan.network)
-def deleteTempVlanNetworks(apps, editor):
- tempVlanNetwork = apps.get_model("resource_inventory", "tempVlanNetwork")
- tempVlanNetwork.objects.all().delete()
-
-
-def pairVlanPhysicalNetworks(apps, editor):
- PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork")
- tempVlanPair = apps.get_model("resource_inventory", "tempVlanNetwork")
- for pair in tempVlanPair.objects.all():
- physicalNetwork = PhysicalNetwork.objects.create(vlan_id=vlan.vlan_id,
- generic_network=pair.network)
- pair.vlan.network = physicalNetwork
def deletePhysicalNetworks(apps, editor):
+ Vlan = apps.get_model("resource_inventory", "Vlan")
+ for vlan in Vlan.objects.all():
+ vlan.network = None
PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork")
PhysicalNetwork.objects.all().delete()
+
class Migration(migrations.Migration):
dependencies = [
@@ -56,21 +49,11 @@ class Migration(migrations.Migration):
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
- migrations.CreateModel(
- name='tempVlanNetwork',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('vlan', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.vlan')),
- ('network', models.ForeignKey(null=True, to='resource_inventory.network', on_delete=django.db.models.deletion.CASCADE)),
- ]
- ),
- migrations.RunPython(genTempVlanNetwork, deleteTempVlanNetworks),
+ migrations.RunPython(pairVlanPhysicalNetworks, deletePhysicalNetworks),
migrations.AlterField(
model_name='vlan',
name='network',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING,
to='resource_inventory.PhysicalNetwork', null=True),
),
- migrations.RunPython(pairVlanPhysicalNetworks, deletePhysicalNetworks),
- migrations.DeleteModel("tempVlanNetwork")
]
diff --git a/src/resource_inventory/migrations/0012_manual_20200218_1536.py b/src/resource_inventory/migrations/0012_manual_20200218_1536.py
new file mode 100644
index 0000000..378bdc3
--- /dev/null
+++ b/src/resource_inventory/migrations/0012_manual_20200218_1536.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.conf import settings
+from django.db import migrations
+
+
+def clear_networks(apps, schema_editor):
+ Network = apps.get_model('resource_inventory', 'Network')
+ Vlan = apps.get_model('resource_inventory', 'Vlan')
+ for vlan in Vlan.objects.all():
+ vlan.delete()
+ for net in Network.objects.all():
+ net.delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('resource_inventory', '0012_auto_20200103_1850'),
+ ]
+
+ operations = [
+ migrations.RunPython(clear_networks)
+ ]
diff --git a/src/resource_inventory/migrations/0013_auto_20200218_1536.py b/src/resource_inventory/migrations/0013_auto_20200218_1536.py
new file mode 100644
index 0000000..014cb2f
--- /dev/null
+++ b/src/resource_inventory/migrations/0013_auto_20200218_1536.py
@@ -0,0 +1,404 @@
+# Generated by Django 2.2 on 2020-02-18 15:36
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import resource_inventory.models
+
+
+def clear_resource_bundles(apps, schema_editor):
+ ResourceBundle = apps.get_model('resource_inventory', 'ResourceBundle')
+ for rb in ResourceBundle.objects.all():
+ rb.template = None
+ rb.save()
+
+
+def create_default_template(apps, schema_editor):
+ ResourceTemplate = apps.get_model('resource_inventory', 'ResourceTemplate')
+ ResourceTemplate.objects.create(id=1, name="Default Template")
+
+
+def populate_servers(apps, schema_editor):
+ """Convert old Host models to Server Resources."""
+ Host = apps.get_model('resource_inventory', 'Host')
+ Server = apps.get_model('resource_inventory', 'Server')
+ ResourceProfile = apps.get_model('resource_inventory', 'ResourceProfile')
+ for h in Host.objects.all():
+ rp = ResourceProfile.objects.get(id=h.profile.id)
+ Server.objects.create(
+ working=h.working,
+ vendor=h.vendor,
+ labid=h.labid,
+ booked=h.booked,
+ name=h.labid,
+ lab=h.lab,
+ profile=rp
+ )
+
+
+def populate_resource_templates(apps, schema_editor):
+ """
+ Convert old GenericResourceBundles to ResourceTemplate.
+
+ This will be kept blank for now. If, during testing, we realize
+ we want to implement this, we will. For now, it seems
+ fine to let the old models just die and create
+ new ones as needed.
+ """
+ pass
+
+
+def populate_resource_profiles(apps, schema_editor):
+ """
+ Convert old HostProfile models to ResourceProfiles.
+
+ Also updates all the foreign keys pointed to the old
+ host profile. This change was basically only a name change.
+ """
+ HostProfile = apps.get_model('resource_inventory', 'HostProfile')
+ ResourceProfile = apps.get_model('resource_inventory', 'ResourceProfile')
+ for hp in HostProfile.objects.all():
+ rp = ResourceProfile.objects.create(id=hp.id, name=hp.name, description=hp.description)
+ rp.labs.add(*list(hp.labs.all()))
+ """
+ TODO: link these models together
+ rp.interfaceprofile = hp.interfaceprofile
+ rp.storageprofile = hp.storageprofile
+ rp.cpuprofile = hp.cpuprofile
+ rp.ramprofile = hp.ramprofile
+ rp.save()
+ hp.interfaceprofile.host = rp
+ rp.storageprofile.host = rp
+ rp.cpuprofile.host = rp
+ rp.ramprofile.host = rp
+ rp.interfaceprofile.save()
+ rp.storageprofile.save()
+ rp.cpuprofile.save()
+ rp.ramprofile.save()
+ """
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('booking', '0007_remove_booking_config_bundle'),
+ ('account', '0004_downtime'),
+ ('api', '0013_manual_20200218_1536'),
+ ('resource_inventory', '0012_manual_20200218_1536'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='InterfaceConfiguration',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('connections', models.ManyToManyField(to='resource_inventory.NetworkConnection')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceConfiguration',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('is_head_node', models.BooleanField(default=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceOPNFVConfig',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ResourceProfile',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ('description', models.TextField()),
+ ('labs', models.ManyToManyField(related_name='resourceprofiles', to='account.Lab')),
+ ],
+ ),
+ migrations.RunPython(populate_resource_profiles),
+ migrations.CreateModel(
+ name='ResourceTemplate',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=300, unique=True)),
+ ('xml', models.TextField()),
+ ('description', models.CharField(default='', max_length=1000)),
+ ('public', models.BooleanField(default=False)),
+ ('hidden', models.BooleanField(default=False)),
+ ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resourcetemplates', to='account.Lab')),
+ ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.RunPython(populate_resource_templates),
+ migrations.CreateModel(
+ name='Server',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('working', models.BooleanField(default=True)),
+ ('vendor', models.CharField(default='unknown', max_length=100)),
+ ('model', models.CharField(default='unknown', max_length=150)),
+ ('labid', models.CharField(default='default_id', max_length=200, unique=True)),
+ ('booked', models.BooleanField(default=False)),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='bundle',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle'),
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='config',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceConfiguration'),
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='interfaces',
+ field=models.ManyToManyField(to='resource_inventory.Interface'),
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='lab',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab'),
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AddField(
+ model_name='server',
+ name='remote_management',
+ field=models.ForeignKey(default=resource_inventory.models.get_default_remote_info, on_delete=models.SET(resource_inventory.models.get_default_remote_info), to='resource_inventory.RemoteInfo'),
+ ),
+ migrations.RunPython(populate_servers),
+ migrations.RemoveField(
+ model_name='generichost',
+ name='profile',
+ ),
+ migrations.RemoveField(
+ model_name='generichost',
+ name='resource',
+ ),
+ migrations.RemoveField(
+ model_name='genericinterface',
+ name='connections',
+ ),
+ migrations.RemoveField(
+ model_name='genericinterface',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='genericinterface',
+ name='profile',
+ ),
+ migrations.RemoveField(
+ model_name='genericresource',
+ name='bundle',
+ ),
+ migrations.RemoveField(
+ model_name='genericresourcebundle',
+ name='lab',
+ ),
+ migrations.RemoveField(
+ model_name='genericresourcebundle',
+ name='owner',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='bundle',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='config',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='lab',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='profile',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='remote_management',
+ ),
+ migrations.RemoveField(
+ model_name='host',
+ name='template',
+ ),
+ migrations.RemoveField(
+ model_name='hostconfiguration',
+ name='bundle',
+ ),
+ migrations.RemoveField(
+ model_name='hostconfiguration',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='hostconfiguration',
+ name='image',
+ ),
+ migrations.RemoveField(
+ model_name='hostopnfvconfig',
+ name='host_config',
+ ),
+ migrations.RemoveField(
+ model_name='hostopnfvconfig',
+ name='opnfv_config',
+ ),
+ migrations.RemoveField(
+ model_name='hostopnfvconfig',
+ name='role',
+ ),
+ migrations.RemoveField(
+ model_name='hostprofile',
+ name='labs',
+ ),
+ migrations.RemoveField(
+ model_name='interface',
+ name='host',
+ ),
+ migrations.RemoveField(
+ model_name='interface',
+ name='name',
+ ),
+ migrations.RemoveField(
+ model_name='opnfvconfig',
+ name='bundle',
+ ),
+ migrations.AddField(
+ model_name='interface',
+ name='profile',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='interfaceprofile',
+ name='order',
+ field=models.IntegerField(default=-1),
+ ),
+ migrations.AlterField(
+ model_name='cpuprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpuprofile', to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AlterField(
+ model_name='diskprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storageprofile', to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AlterField(
+ model_name='image',
+ name='host_type',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AlterField(
+ model_name='interfaceprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaceprofile', to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AlterField(
+ model_name='network',
+ name='bundle',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='networks', to='resource_inventory.ResourceTemplate'),
+ ),
+ migrations.AlterField(
+ model_name='ramprofile',
+ name='host',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ramprofile', to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.RunPython(clear_resource_bundles),
+ migrations.AlterField(
+ model_name='resourcebundle',
+ name='template',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceTemplate'),
+ ),
+ migrations.DeleteModel(
+ name='ConfigBundle',
+ ),
+ migrations.DeleteModel(
+ name='GenericHost',
+ ),
+ migrations.DeleteModel(
+ name='GenericInterface',
+ ),
+ migrations.DeleteModel(
+ name='GenericResource',
+ ),
+ migrations.DeleteModel(
+ name='GenericResourceBundle',
+ ),
+ migrations.DeleteModel(
+ name='HostConfiguration',
+ ),
+ migrations.DeleteModel(
+ name='HostOPNFVConfig',
+ ),
+ migrations.DeleteModel(
+ name='HostProfile',
+ ),
+ migrations.DeleteModel(
+ name='Host',
+ ),
+ migrations.AddField(
+ model_name='resourceopnfvconfig',
+ name='opnfv_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_config', to='resource_inventory.OPNFVConfig'),
+ ),
+ migrations.AddField(
+ model_name='resourceopnfvconfig',
+ name='resource_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_config', to='resource_inventory.ResourceConfiguration'),
+ ),
+ migrations.AddField(
+ model_name='resourceopnfvconfig',
+ name='role',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_configs', to='resource_inventory.OPNFVRole'),
+ ),
+ migrations.AddField(
+ model_name='resourceconfiguration',
+ name='image',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.Image'),
+ ),
+ migrations.AddField(
+ model_name='resourceconfiguration',
+ name='profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'),
+ ),
+ migrations.AddField(
+ model_name='resourceconfiguration',
+ name='template',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='resourceConfigurations', to='resource_inventory.ResourceTemplate'),
+ ),
+ migrations.AddField(
+ model_name='interfaceconfiguration',
+ name='profile',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'),
+ ),
+ migrations.AddField(
+ model_name='interfaceconfiguration',
+ name='resource_config',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interface_configs', to='resource_inventory.ResourceConfiguration'),
+ ),
+ migrations.AddField(
+ model_name='interface',
+ name='acts_as',
+ field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.InterfaceConfiguration'),
+ ),
+ migrations.RunPython(create_default_template),
+ migrations.AddField(
+ model_name='opnfvconfig',
+ name='template',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opnfv_config', to='resource_inventory.ResourceTemplate'),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py
index a8b75d9..d11f71b 100644
--- a/src/resource_inventory/models.py
+++ b/src/resource_inventory/models.py
@@ -8,21 +8,30 @@
##############################################################################
from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
from django.db import models
-from django.core.validators import RegexValidator
+from django.db.models import Q
import re
from account.models import Lab
+from dashboard.utils import AbstractModelQuery
-# profile of resources hosted by labs
-class HostProfile(models.Model):
+"""
+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)
- host_type = models.PositiveSmallIntegerField(default=0)
name = models.CharField(max_length=200, unique=True)
description = models.TextField()
- labs = models.ManyToManyField(Lab, related_name="hostprofiles")
+ labs = models.ManyToManyField(Lab, related_name="resourceprofiles")
def validate(self):
validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$")
@@ -34,12 +43,33 @@ class HostProfile(models.Model):
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(HostProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
+ host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
nic_type = models.CharField(
max_length=50,
choices=[
@@ -48,6 +78,7 @@ class InterfaceProfile(models.Model):
],
default="onboard"
)
+ order = models.IntegerField(default=-1)
def __str__(self):
return self.name + " for " + str(self.host)
@@ -61,7 +92,7 @@ class DiskProfile(models.Model):
("HDD", "HDD")
])
name = models.CharField(max_length=50)
- host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='storageprofile')
+ host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile')
rotation = models.IntegerField(default=0)
interface = models.CharField(
max_length=50,
@@ -88,7 +119,7 @@ class CpuProfile(models.Model):
("aarch64", "aarch64")
])
cpus = models.IntegerField()
- host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='cpuprofile')
+ host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile')
cflags = models.TextField(null=True)
def __str__(self):
@@ -99,16 +130,116 @@ class RamProfile(models.Model):
id = models.AutoField(primary_key=True)
amount = models.IntegerField()
channels = models.IntegerField()
- host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='ramprofile')
+ 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 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, unique=True)
+ 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)
+ hidden = models.BooleanField(default=False)
+
+ def getConfigs(self):
+ return list(self.resourceConfigurations.all())
+
+ 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
+
+
+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?
+
+ def __str__(self):
+ return "config with " + str(self.template) + " and image " + str(self.image)
+
+
+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, null=True)
+ profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
+ config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, 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.
@@ -129,45 +260,129 @@ class Resource(models.Model):
def get_interfaces(self):
"""
- Returns a list of interfaces on this resource.
+ 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)
-# Generic resource templates
-class GenericResourceBundle(models.Model):
+
+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"
+
+ return {
+ "id": self.labid,
+ "image": self.config.image.lab_id,
+ "hostname": self.template.resource.name,
+ "power": power,
+ "ipmi_create": str(ipmi)
+ }
+
+ def get_interfaces(self):
+ return list(self.interfaces.all().order_by('bus_address'))
+
+ def release(self):
+ 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
+
+
+class Opsys(models.Model):
id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=300, unique=True)
- 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)
- description = models.CharField(max_length=1000, default="")
- public = models.BooleanField(default=False)
- hidden = models.BooleanField(default=False)
+ name = models.CharField(max_length=100)
+ sup_installers = models.ManyToManyField("Installer", blank=True)
+
+ def __str__(self):
+ return self.name
- def getResources(self):
- my_resources = []
- for genericResource in self.generic_resources.all():
- my_resources.append(genericResource.getResource())
- return my_resources
+class Image(models.Model):
+ """Model for representing OS images / snapshots of hosts."""
+
+ id = models.AutoField(primary_key=True)
+ lab_id = models.IntegerField() # ID the lab who holds this image knows
+ from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
+ name = models.CharField(max_length=200)
+ owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
+ public = models.BooleanField(default=True)
+ host_type = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
+ description = models.TextField()
+ os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
def __str__(self):
return self.name
+ 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=100)
- bundle = models.ForeignKey(GenericResourceBundle, on_delete=models.CASCADE, related_name="networks")
+ bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks")
is_public = models.BooleanField()
def __str__(self):
return self.name
-class PhysicalNetwork(Resource):
+class PhysicalNetwork(models.Model):
vlan_id = models.IntegerField()
generic_network = models.ForeignKey(Network, on_delete=models.CASCADE)
@@ -205,65 +420,21 @@ class Vlan(models.Model):
return str(self.vlan_id) + ("_T" if self.tagged else "")
-class ConfigState:
- NEW = 0
- RESET = 100
- CLEAN = 200
-
-
-class GenericResource(models.Model):
- bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.CASCADE)
- hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)")
- name = models.CharField(max_length=200, validators=[hostname_validchars])
-
- def getResource(self):
- # TODO: This will have to be dealt with
- return self.generic_host
-
- def __str__(self):
- return self.name
-
- def validate(self):
- validname = re.compile(r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))')
- if not validname.match(self.name):
- return "Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)"
- else:
- return None
-
-
-# Host template
-class GenericHost(models.Model):
- id = models.AutoField(primary_key=True)
- profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
- resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.CASCADE)
-
- def __str__(self):
- return self.resource.name
-
-
-# Physical, actual resources
-class ResourceBundle(Resource):
- template = models.ForeignKey(GenericResourceBundle, 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_host(self, role="Jumphost"):
- return Host.objects.filter(bundle=self, config__is_head_node=True).first() # should only ever be one, but it is not an invariant in the models
-
-
-class GenericInterface(models.Model):
+class InterfaceConfiguration(models.Model):
id = models.AutoField(primary_key=True)
profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
- host = models.ForeignKey(GenericHost, on_delete=models.CASCADE, related_name='generic_interfaces')
+ resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs')
connections = models.ManyToManyField(NetworkConnection)
def __str__(self):
return "type " + str(self.profile) + " on host " + str(self.host)
+"""
+OPNFV / Software configuration models
+"""
+
+
class Scenario(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=300)
@@ -281,38 +452,16 @@ class Installer(models.Model):
return self.name
-class Opsys(models.Model):
- id = models.AutoField(primary_key=True)
- name = models.CharField(max_length=100)
- sup_installers = models.ManyToManyField(Installer, 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)
-class ConfigBundle(models.Model):
- id = models.AutoField(primary_key=True)
- owner = models.ForeignKey(User, on_delete=models.CASCADE)
- name = models.CharField(max_length=200, unique=True)
- description = models.CharField(max_length=1000, default="")
- bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE)
- public = models.BooleanField(default=False)
- hidden = models.BooleanField(default=False)
-
- def __str__(self):
- return self.name
-
-
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)
- bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", 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="")
@@ -330,105 +479,14 @@ class OPNFVRole(models.Model):
return self.name
-class Image(models.Model):
- """Model for representing OS images / snapshots of hosts."""
-
- id = models.AutoField(primary_key=True)
- lab_id = models.IntegerField() # ID the lab who holds this image knows
- from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
- name = models.CharField(max_length=200)
- owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
- public = models.BooleanField(default=True)
- host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
- description = models.TextField()
- os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
-
- def __str__(self):
- return self.name
-
- def in_use(self):
- return Host.objects.filter(booked=True, config__image=self).exists()
-
-
def get_sentinal_opnfv_role():
return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
-class HostConfiguration(models.Model):
- """Model to represent a complete configuration for a single physical host."""
-
- id = models.AutoField(primary_key=True)
- host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE)
- image = models.ForeignKey(Image, on_delete=models.PROTECT)
- bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE)
- is_head_node = models.BooleanField(default=False)
-
- def __str__(self):
- return "config with " + str(self.host) + " and image " + str(self.image)
-
-
-class HostOPNFVConfig(models.Model):
- role = models.ForeignKey(OPNFVRole, related_name="host_opnfv_configs", on_delete=models.CASCADE)
- host_config = models.ForeignKey(HostConfiguration, related_name="host_opnfv_config", on_delete=models.CASCADE)
- opnfv_config = models.ForeignKey(OPNFVConfig, related_name="host_opnfv_config", on_delete=models.CASCADE)
-
-
-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
-
-
-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
-
-
-# Concrete host, actual machine in a lab
-class Host(Resource):
- template = models.ForeignKey(GenericHost, on_delete=models.SET_NULL, null=True)
- booked = models.BooleanField(default=False)
- name = models.CharField(max_length=200, unique=True)
- bundle = models.ForeignKey(ResourceBundle, related_name='hosts', on_delete=models.SET_NULL, null=True)
- config = models.ForeignKey(HostConfiguration, null=True, related_name="configuration", on_delete=models.SET_NULL)
- labid = models.CharField(max_length=200, default="default_id")
- profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE)
- lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
- working = models.BooleanField(default=True)
- vendor = models.CharField(max_length=100, default="unknown")
- model = models.CharField(max_length=150, default="unknown")
- remote_management = models.ForeignKey(RemoteInfo, default=get_default_remote_info, on_delete=models.SET(get_default_remote_info))
-
- def __str__(self):
- return self.name
-
- def get_configuration(self, state):
- ipmi = state == ConfigState.NEW
- power = "off" if state == ConfigState.CLEAN else "on"
-
- return {
- "id": self.labid,
- "image": self.config.image.lab_id,
- "hostname": self.template.resource.name,
- "power": power,
- "ipmi_create": str(ipmi)
- }
-
- def release(self):
- self.booked = False
- self.save()
-
- def get_interfaces(self):
- return list(self.interfaces.all().order_by('bus_address'))
+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):
@@ -436,15 +494,33 @@ class Interface(models.Model):
mac_address = models.CharField(max_length=17)
bus_address = models.CharField(max_length=50)
config = models.ManyToManyField(Vlan)
- host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='interfaces')
+ acts_as = models.OneToOneField(InterfaceConfiguration, null=True, on_delete=models.SET_NULL)
profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
def __str__(self):
return self.mac_address + " on host " + str(self.host)
+"""
+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]
diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py
index 51e3746..6844b09 100644
--- a/src/resource_inventory/pdf_templater.py
+++ b/src/resource_inventory/pdf_templater.py
@@ -10,7 +10,7 @@
from django.template.loader import render_to_string
import booking
-from resource_inventory.models import Host, InterfaceProfile
+from resource_inventory.models import Server, InterfaceProfile
class PDFTemplater:
@@ -66,7 +66,7 @@ class PDFTemplater:
)
jumphost = booking.resource.hosts.get(config=jumphost_opnfv_config.host_config)
else: # if there is no opnfv config, use headnode
- jumphost = Host.objects.filter(
+ jumphost = Server.objects.filter(
bundle=booking.resource,
config__is_head_node=True
).first()
@@ -85,7 +85,7 @@ class PDFTemplater:
def get_pdf_nodes(cls, booking):
"""Return a list of all the "nodes" (every host except jumphost)."""
pdf_nodes = []
- nodes = set(Host.objects.filter(bundle=booking.resource))
+ nodes = set(Server.objects.filter(bundle=booking.resource))
nodes.discard(cls.get_jumphost(booking))
for node in nodes:
diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py
index 242d21a..c8b2b05 100644
--- a/src/resource_inventory/resource_manager.py
+++ b/src/resource_inventory/resource_manager.py
@@ -7,18 +7,13 @@
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
import re
+from django.db.models import Q
+
+from dashboard.exceptions import ResourceAvailabilityException
-from dashboard.exceptions import (
- ResourceExistenceException,
- ResourceAvailabilityException,
- ResourceProvisioningException,
- ModelValidationException,
-)
from resource_inventory.models import (
- Host,
- HostConfiguration,
ResourceBundle,
- HostProfile,
+ ResourceTemplate,
Network,
Vlan,
PhysicalNetwork,
@@ -38,32 +33,27 @@ class ResourceManager:
ResourceManager.instance = ResourceManager()
return ResourceManager.instance
- def getAvailableHostTypes(self, lab):
- hostset = Host.objects.filter(lab=lab).filter(booked=False).filter(working=True)
- hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab)
- return set(hostprofileset)
+ def getAvailableResourceTemplates(self, lab, user):
+ templates = ResourceTemplate.objects.filter(lab=lab)
+ templates.filter(Q(owner=user) | Q(public=True))
+ return templates
- def hostsAvailable(self, grb):
+ def templateIsReservable(self, resource_template):
"""
- Check if the given GenericResourceBundle is available.
+ Check if the required resources to reserve this template is available.
No changes to the database
"""
# count up hosts
profile_count = {}
- for host in grb.getResources():
- if host.profile not in profile_count:
- profile_count[host.profile] = 0
- profile_count[host.profile] += 1
+ 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 = Host.objects.filter(
- booked=False,
- working=True,
- lab=grb.lab,
- profile=profile
- ).count()
+ available = len(profile.get_resources(lab=resource_template.lab, unreserved=True))
needed = profile_count[profile]
if available < needed:
return False
@@ -71,8 +61,8 @@ class ResourceManager:
# public interface
def deleteResourceBundle(self, resourceBundle):
- for host in Host.objects.filter(bundle=resourceBundle):
- host.release()
+ for resource in resourceBundle.get_resources():
+ resource.release()
resourceBundle.delete()
def get_vlans(self, genericResourceBundle):
@@ -89,43 +79,32 @@ class ResourceManager:
networks[network.name] = vlan
return networks
- def convertResourceBundle(self, genericResourceBundle, config=None):
+ def instantiateTemplate(self, resource_template, config=None):
"""
- Convert a GenericResourceBundle into a ResourceBundle.
+ Convert a ResourceTemplate into a ResourceBundle.
- Takes in a genericResourceBundle and reserves all the
+ Takes in a ResourceTemplate and reserves all the
Resources needed and returns a completed ResourceBundle.
"""
- resource_bundle = ResourceBundle.objects.create(template=genericResourceBundle)
- generic_hosts = genericResourceBundle.getResources()
- physical_hosts = []
+ resource_bundle = ResourceBundle.objects.create(template=resource_template)
+ res_configs = resource_template.getConfigs()
+ resources = []
- vlan_map = self.get_vlans(genericResourceBundle)
+ vlan_map = self.get_vlans(resource_template)
- for generic_host in generic_hosts:
- host_config = None
- if config:
- host_config = HostConfiguration.objects.get(bundle=config, host=generic_host)
- try:
- physical_host = self.acquireHost(generic_host, genericResourceBundle.lab.name)
- except ResourceAvailabilityException:
- self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
- raise ResourceAvailabilityException("Could not provision hosts, not enough available")
+ for config in res_configs:
try:
- physical_host.bundle = resource_bundle
- physical_host.template = generic_host
- physical_host.config = host_config
- physical_hosts.append(physical_host)
-
- self.configureNetworking(physical_host, vlan_map)
- except Exception:
- self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
- raise ResourceProvisioningException("Network configuration failed.")
- try:
- physical_host.save()
- except Exception:
- self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle)
- raise ModelValidationException("Saving hosts failed")
+ phys_res = self.acquireHost(config)
+ phys_res.bundle = resource_bundle
+ phys_res.config = config
+ resources.append(phys_res)
+
+ self.configureNetworking(phys_res, vlan_map)
+ phys_res.save()
+
+ except Exception as e:
+ self.fail_acquire(resources, vlan_map, resource_template)
+ raise e
return resource_bundle
@@ -149,30 +128,27 @@ class ResourceManager:
)
# private interface
- def acquireHost(self, genericHost, labName):
- host_full_set = Host.objects.filter(lab__name__exact=labName, profile=genericHost.profile)
- if not host_full_set.first():
- raise ResourceExistenceException("No matching servers found")
- host_set = host_full_set.filter(booked=False, working=True)
- if not host_set.first():
- raise ResourceAvailabilityException("No unbooked hosts match requested hosts")
- host = host_set.first()
- host.booked = True
- host.template = genericHost
- host.save()
- return host
-
- def releaseNetworks(self, grb, vlan_manager, vlans):
+ def acquireHost(self, resource_config):
+ resources = resource_config.profile.get_resources(lab=resource_config.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=grb)
+ 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, grb):
- vlan_manager = grb.lab.vlan_manager
- self.releaseNetworks(grb, vlan_manager, vlans)
+ def fail_acquire(self, hosts, vlans, template):
+ self.releaseNetworks(template, vlans)
for host in hosts:
host.release()
diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py
index 8c3d899..52f8c75 100644
--- a/src/resource_inventory/views.py
+++ b/src/resource_inventory/views.py
@@ -12,7 +12,7 @@ from django.views.generic import TemplateView
from django.shortcuts import get_object_or_404
from django.shortcuts import render
-from resource_inventory.models import HostProfile, Host
+from resource_inventory.models import ResourceProfile, ResourceQuery
class HostView(TemplateView):
@@ -20,13 +20,13 @@ class HostView(TemplateView):
def get_context_data(self, **kwargs):
context = super(HostView, self).get_context_data(**kwargs)
- hosts = Host.objects.filter(working=True)
+ 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(HostProfile, id=hostprofile_id)
+ hostprofile = get_object_or_404(ResourceProfile, id=hostprofile_id)
return render(
request,
diff --git a/src/workflow/booking_workflow.py b/src/workflow/booking_workflow.py
index c96e1b9..00fa0f9 100644
--- a/src/workflow/booking_workflow.py
+++ b/src/workflow/booking_workflow.py
@@ -14,7 +14,7 @@ from datetime import timedelta
from booking.models import Booking
from workflow.models import WorkflowStep, AbstractSelectOrCreate
from workflow.forms import ResourceSelectorForm, SWConfigSelectorForm, BookingMetaForm, OPNFVSelectForm
-from resource_inventory.models import GenericResourceBundle, ConfigBundle, OPNFVConfig
+from resource_inventory.models import OPNFVConfig, ResourceTemplate
from django.db.models import Q
@@ -44,8 +44,7 @@ class Abstract_Resource_Select(AbstractSelectOrCreate):
def get_form_queryset(self):
user = self.repo_get(self.repo.SESSION_USER)
- qs = GenericResourceBundle.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True)))
- return qs
+ return ResourceTemplate.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True)))
def get_page_context(self):
return {
@@ -83,7 +82,7 @@ class SWConfig_Select(AbstractSelectOrCreate):
def get_form_queryset(self):
user = self.repo_get(self.repo.SESSION_USER)
grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
- qs = ConfigBundle.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True))).filter(bundle=grb)
+ qs = ResourceTemplate.objects.filter(Q(hidden=False) & (Q(owner=user) | Q(public=True))).filter(bundle=grb)
return qs
def put_confirm_info(self, bundle):
diff --git a/src/workflow/forms.py b/src/workflow/forms.py
index f7a20eb..37bc390 100644
--- a/src/workflow/forms.py
+++ b/src/workflow/forms.py
@@ -300,7 +300,7 @@ class FormUtils:
else:
multiple_hosts = false
labs = {}
- hosts = {}
+ resources = {}
items = {}
neighbors = {}
for lab in Lab.objects.all():
@@ -311,24 +311,21 @@ class FormUtils:
'description': lab.description,
'selected': false,
'selectable': true,
- 'follow': false,
+ 'follow': multiple_hosts,
'multiple': false,
'class': 'lab'
}
- if multiple_hosts:
- # "follow" this lab node to discover more hosts if allowed
- lab_node['follow'] = true
items[lab_node['id']] = lab_node
neighbors[lab_node['id']] = []
labs[lab_node['id']] = lab_node
- for host in lab.hostprofiles.all():
- host_node = {
+ for template in lab.resourcetemplates.all():
+ resource_node = {
'form': {"name": "host_name", "type": "text", "placeholder": "hostname"},
- 'id': "host_" + str(host.id),
- 'model_id': host.id,
- 'name': host.name,
- 'description': host.description,
+ 'id': "resource_" + str(template.id),
+ 'model_id': template.id,
+ 'name': template.name,
+ 'description': template.description,
'selected': false,
'selectable': true,
'follow': false,
@@ -336,15 +333,15 @@ class FormUtils:
'class': 'host'
}
if multiple_hosts:
- host_node['values'] = [] # place to store multiple values
- items[host_node['id']] = host_node
- neighbors[lab_node['id']].append(host_node['id'])
- if host_node['id'] not in neighbors:
- neighbors[host_node['id']] = []
- neighbors[host_node['id']].append(lab_node['id'])
- hosts[host_node['id']] = host_node
-
- display_objects = [("lab", labs.values()), ("host", hosts.values())]
+ 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,
diff --git a/src/workflow/models.py b/src/workflow/models.py
index 32ac39c..df00d21 100644
--- a/src/workflow/models.py
+++ b/src/workflow/models.py
@@ -18,7 +18,7 @@ import requests
from workflow.forms import ConfirmationForm
from api.models import JobFactory
from dashboard.exceptions import ResourceAvailabilityException, ModelValidationException
-from resource_inventory.models import Image, GenericInterface, OPNFVConfig, HostOPNFVConfig, NetworkRole
+from resource_inventory.models import Image, InterfaceConfiguration, OPNFVConfig, ResourceOPNFVConfig, NetworkRole
from resource_inventory.resource_manager import ResourceManager
from resource_inventory.pdf_templater import PDFTemplater
from notifier.manager import NotificationHandler
@@ -552,7 +552,7 @@ class Repository():
if 'connections' in models:
for resource_name, mapping in models['connections'].items():
for profile_name, connection_set in mapping.items():
- interface = GenericInterface.objects.get(
+ interface = InterfaceConfiguration.objects.get(
profile__name=profile_name,
host__resource__name=resource_name,
host__resource__bundle=models['bundle']
@@ -725,7 +725,7 @@ class Repository():
config = config_bundle.hostConfigurations.get(
host__resource__name=host_role['host_name']
)
- HostOPNFVConfig.objects.create(
+ ResourceOPNFVConfig.objects.create(
role=host_role['role'],
host_config=config,
opnfv_config=opnfv_config
diff --git a/src/workflow/opnfv_workflow.py b/src/workflow/opnfv_workflow.py
index 0cac48e..6ffc91d 100644
--- a/src/workflow/opnfv_workflow.py
+++ b/src/workflow/opnfv_workflow.py
@@ -11,7 +11,7 @@
from django.forms import formset_factory
from workflow.models import WorkflowStep, AbstractSelectOrCreate
-from resource_inventory.models import ConfigBundle, OPNFV_SETTINGS
+from resource_inventory.models import ResourceTemplate, OPNFV_SETTINGS
from workflow.forms import OPNFVSelectionForm, OPNFVNetworkRoleForm, OPNFVHostRoleForm, SWConfigSelectorForm, BasicMetaForm
@@ -27,7 +27,7 @@ class OPNFV_Resource_Select(AbstractSelectOrCreate):
def get_form_queryset(self):
user = self.repo_get(self.repo.SESSION_USER)
- qs = ConfigBundle.objects.filter(owner=user)
+ qs = ResourceTemplate.objects.filter(owner=user)
return qs
def put_confirm_info(self, bundle):
diff --git a/src/workflow/resource_bundle_workflow.py b/src/workflow/resource_bundle_workflow.py
index f57476b..89baae7 100644
--- a/src/workflow/resource_bundle_workflow.py
+++ b/src/workflow/resource_bundle_workflow.py
@@ -22,11 +22,10 @@ from workflow.forms import (
ResourceMetaForm,
)
from resource_inventory.models import (
- GenericResourceBundle,
- GenericInterface,
- GenericHost,
- GenericResource,
- HostProfile,
+ ResourceProfile,
+ ResourceTemplate,
+ ResourceConfiguration,
+ InterfaceConfiguration,
Network,
NetworkConnection
)
@@ -64,12 +63,12 @@ class Define_Hardware(WorkflowStep):
models['hosts'] = [] # This will always clear existing data when this step changes
models['interfaces'] = {}
if "bundle" not in models:
- models['bundle'] = GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER))
host_data = data['host']
names = {}
for host_profile_dict in host_data.values():
id = host_profile_dict['id']
- profile = HostProfile.objects.get(id=id)
+ profile = ResourceProfile.objects.get(id=id)
# instantiate genericHost and store in repo
for name in host_profile_dict['values'].values():
if not re.match(r"(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})", name):
@@ -77,14 +76,13 @@ class Define_Hardware(WorkflowStep):
if name in names:
raise NonUniqueHostnameException("All hosts must have unique names")
names[name] = True
- genericResource = GenericResource(bundle=models['bundle'], name=name)
- genericHost = GenericHost(profile=profile, resource=genericResource)
- models['hosts'].append(genericHost)
+ resourceConfig = ResourceConfiguration(profile=profile, template=models['bundle'])
+ models['hosts'].append(resourceConfig)
for interface_profile in profile.interfaceprofile.all():
- genericInterface = GenericInterface(profile=interface_profile, host=genericHost)
- if genericHost.resource.name not in models['interfaces']:
- models['interfaces'][genericHost.resource.name] = []
- models['interfaces'][genericHost.resource.name].append(genericInterface)
+ genericInterface = InterfaceConfiguration(profile=interface_profile, resource_config=resourceConfig)
+ if resourceConfig.name not in models['interfaces']:
+ models['interfaces'][resourceConfig.name] = []
+ models['interfaces'][resourceConfig.name].append(genericInterface)
# add selected lab to models
for lab_dict in data['lab'].values():
@@ -226,7 +224,7 @@ class Define_Nets(WorkflowStep):
for host in existing_host_list:
existing_hosts[host.resource.name] = host
- bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ bundle = models.get("bundle", ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER)))
for net_id, net in networks.items():
network = Network()
@@ -381,7 +379,7 @@ class Resource_Meta_Info(WorkflowStep):
models = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {})
name = form.cleaned_data['bundle_name']
desc = form.cleaned_data['bundle_description']
- bundle = models.get("bundle", GenericResourceBundle(owner=self.repo_get(self.repo.SESSION_USER)))
+ bundle = models.get("bundle", ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER)))
bundle.name = name
bundle.description = desc
models['bundle'] = bundle
diff --git a/src/workflow/snapshot_workflow.py b/src/workflow/snapshot_workflow.py
index c2f4cd6..c0e2052 100644
--- a/src/workflow/snapshot_workflow.py
+++ b/src/workflow/snapshot_workflow.py
@@ -12,7 +12,7 @@ from django.utils import timezone
import json
from booking.models import Booking
-from resource_inventory.models import Host, Image
+from resource_inventory.models import ResourceQuery, Image
from workflow.models import WorkflowStep
from workflow.forms import BasicMetaForm, SnapshotHostSelectForm
@@ -61,7 +61,7 @@ class Select_Host_Step(WorkflowStep):
name = host['name']
booking_id = host['booking']
booking = Booking.objects.get(pk=booking_id)
- host = Host.objects.get(bundle=booking.resource, template__resource__name=name)
+ 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
diff --git a/src/workflow/sw_bundle_workflow.py b/src/workflow/sw_bundle_workflow.py
index ebd8c86..686f46f 100644
--- a/src/workflow/sw_bundle_workflow.py
+++ b/src/workflow/sw_bundle_workflow.py
@@ -13,7 +13,7 @@ from django.forms import formset_factory
from workflow.models import WorkflowStep
from workflow.forms import BasicMetaForm, HostSoftwareDefinitionForm
from workflow.booking_workflow import Abstract_Resource_Select
-from resource_inventory.models import Image, GenericHost, ConfigBundle, HostConfiguration
+from resource_inventory.models import Image, ResourceConfiguration, ResourceTemplate
class SWConf_Resource_Select(Abstract_Resource_Select):
@@ -38,7 +38,7 @@ class Define_Software(WorkflowStep):
user = self.repo_get(self.repo.SESSION_USER)
lab = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE).lab
for i, host_data in enumerate(hosts_data):
- host = GenericHost.objects.get(pk=host_data['host_id'])
+ host = ResourceConfiguration.objects.get(pk=host_data['host_id'])
wrong_owner = Image.objects.exclude(owner=user).exclude(public=True)
wrong_host = Image.objects.exclude(host_type=host.profile)
wrong_lab = Image.objects.exclude(from_lab=lab)
@@ -86,7 +86,7 @@ class Define_Software(WorkflowStep):
if not grb:
return []
if grb.id:
- return GenericHost.objects.filter(resource__bundle=grb)
+ return ResourceConfiguration.objects.filter(resource__bundle=grb)
generic_hosts = self.repo_get(self.repo.GRESOURCE_BUNDLE_MODELS, {}).get("hosts", [])
return generic_hosts
@@ -109,7 +109,7 @@ class Define_Software(WorkflowStep):
def post(self, post_data, user):
models = self.repo_get(self.repo.CONFIG_MODELS, {})
if "bundle" not in models:
- models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER))
confirm = self.repo_get(self.repo.CONFIRMATION, {})
@@ -127,7 +127,7 @@ class Define_Software(WorkflowStep):
if headnode:
has_headnode = True
bundle = models['bundle']
- hostConfig = HostConfiguration(
+ hostConfig = ResourceConfiguration(
host=host,
image=image,
bundle=bundle,
@@ -175,7 +175,7 @@ class Config_Software(WorkflowStep):
def post(self, post_data, user):
models = self.repo_get(self.repo.CONFIG_MODELS, {})
if "bundle" not in models:
- models['bundle'] = ConfigBundle(owner=self.repo_get(self.repo.SESSION_USER))
+ models['bundle'] = ResourceTemplate(owner=self.repo_get(self.repo.SESSION_USER))
confirm = self.repo_get(self.repo.CONFIRMATION, {})
if "configuration" not in confirm:
diff --git a/src/workflow/workflow_manager.py b/src/workflow/workflow_manager.py
index fda105e..e31e14c 100644
--- a/src/workflow/workflow_manager.py
+++ b/src/workflow/workflow_manager.py
@@ -16,9 +16,8 @@ from booking.models import Booking
from workflow.workflow_factory import WorkflowFactory
from workflow.models import Repository
from resource_inventory.models import (
- GenericResourceBundle,
- ConfigBundle,
- HostConfiguration,
+ ResourceTemplate,
+ ResourceConfiguration,
OPNFVConfig
)
from workflow.forms import ManagerForm
@@ -154,10 +153,10 @@ class SessionManager():
edit_object = Booking.objects.get(pk=target_id)
self.prefill_booking(edit_object)
elif workflow_type == 1:
- edit_object = GenericResourceBundle.objects.get(pk=target_id)
+ edit_object = ResourceTemplate.objects.get(pk=target_id)
self.prefill_resource(edit_object)
elif workflow_type == 2:
- edit_object = ConfigBundle.objects.get(pk=target_id)
+ edit_object = ResourceTemplate.objects.get(pk=target_id)
self.prefill_config(edit_object)
def prefill_booking(self, booking):
@@ -213,7 +212,7 @@ class SessionManager():
models = self.active_workflow().repository.el.get(self.active_workflow().repository.CONFIG_MODELS, {})
models['bundle'] = config
models['host_configs'] = []
- for host_conf in HostConfiguration.objects.filter(bundle=config):
+ 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
@@ -227,7 +226,7 @@ class SessionManager():
opnfv = OPNFVConfig.objects.filter(bundle=config).last()
confirm['configuration']['installer'] = opnfv.installer.name
confirm['configuration']['scenario'] = opnfv.scenario.name
- for host_conf in HostConfiguration.objects.filter(bundle=config):
+ 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