diff options
Diffstat (limited to 'src')
43 files changed, 1370 insertions, 870 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/static/img/akraino_logo.logo b/src/static/img/akraino_logo.logo Binary files differnew file mode 100644 index 0000000..ac85b38 --- /dev/null +++ b/src/static/img/akraino_logo.logo diff --git a/src/templates/akraino/base.html b/src/templates/akraino/base.html new file mode 100644 index 0000000..1368476 --- /dev/null +++ b/src/templates/akraino/base.html @@ -0,0 +1,24 @@ +{% extends "base/base.html" %} +{% load staticfiles %} +{% block bgColor %} +<style> +.bgAkr { + background: #d9c2f2; +} +</style> +<nav class="navbar navbar-light bgAkr navbar-fixed-top border-bottom py-0 mb-0" role="navigation"> +{% endblock bgColor %} + +{% block logo %} +<div class="col-12 col-sm order-1 order-sm-2 text-center text-lg-left"> + <a href="https://www.lfedge.org/projects/akraino/" class="navbar-brand"> + <img src="{% static "img/akraino_logo.logo" %}"> + </a> + + <a class="navbar-brand d-none d-lg-inline" href={% url 'dashboard:index' %}> + Akraino Dashboard + </a> +</div> +{% endblock logo %} +{% block dropDown %} +{% endblock dropDown %} diff --git a/src/templates/akraino/booking/quick_deploy.html b/src/templates/akraino/booking/quick_deploy.html new file mode 100644 index 0000000..56a4791 --- /dev/null +++ b/src/templates/akraino/booking/quick_deploy.html @@ -0,0 +1,11 @@ +{% extends "base/booking/quick_deploy.html" %} +{% block opnfv %} +{% endblock opnfv %} +{% block collab %} +<div class="col-12 col-lg-4 my-2"> + <div class="col border rounded py-2 h-100"> + <label>Collaborators</label> + {{ form.users }} + </div> +</div> +{% endblock collab %} diff --git a/src/templates/akraino/dashboard/landing.html b/src/templates/akraino/dashboard/landing.html new file mode 100644 index 0000000..d7f434b --- /dev/null +++ b/src/templates/akraino/dashboard/landing.html @@ -0,0 +1,22 @@ +{% extends "base/dashboard/landing.html" %} +{% block about_us %} + <p>The Shared Community Lab at the IOL aims to help development and testing of LFN projects by hosting hardware and providing access to the community.</p> + <p>To get started, you can request access to a server at the right.</p> +{% endblock about_us %} + +{% block btnGrp %} +<style> +.btnAkr { + color: #fff; + background-color: #39c0c0; +} +.btnAkr:hover{ + color: #fff; + background-color: #259a9a; +} +</style> +<p>To get started, book a pod below:</p> +<a class="btn btnAkr btn-lg d-flex flex-column justify-content-center align-content-center border text-white p-4" href="/booking/quick/">Book a Pod</a> +{% endblock btnGrp %} +{% block returningUsers %} +{% endblock returningUsers %} diff --git a/src/templates/akraino/layout.html b/src/templates/akraino/layout.html new file mode 100644 index 0000000..d30ddb6 --- /dev/null +++ b/src/templates/akraino/layout.html @@ -0,0 +1,5 @@ +{% extends "base/layout.html" %} + +{% block head-title %} +<title>Akraino Dashboard</title> +{% endblock head-title %} diff --git a/src/templates/base/base.html b/src/templates/base/base.html index 6776f7e..663741a 100644 --- a/src/templates/base/base.html +++ b/src/templates/base/base.html @@ -12,17 +12,19 @@ {% block basecontent %} <div id="wrapper" class="d-flex flex-column"> <!-- Navigation --> + {% block bgColor %} <nav class="navbar navbar-light bg-light navbar-fixed-top border-bottom py-0 mb-0" role="navigation"> + {% endblock bgColor %} <div class="container-fluid pb-2 pb-sm-0"> <!-- Logo --> + {% block logo %} <div class="col-12 col-sm order-1 order-sm-2 text-center text-lg-left"> - <a href="https://www.opnfv.org/" class="navbar-brand"> - <img src="{% static "img/opnfv-logo.png" %}"> - </a> + <h2>Logo Here</h2> <a class="navbar-brand d-none d-lg-inline" href={% url 'dashboard:index' %}> Laas Dashboard </a> </div> + {% endblock logo %} <!-- Sidebar button --> <div class="col-6 col-sm-2 d-flex order-1 order-lg-3 d-lg-none"> <button class="btn border mx-auto" type="button" data-toggle="collapse" data-target="#sidebar" @@ -75,6 +77,7 @@ <a href="/" class="list-group-item list-group-item-action"> Home </a> + {% block dropDown %} <a class="list-group-item list-group-item-action" data-toggle="collapse" href="#createList" role="button"> Create <i class="fas fa-angle-down rotate"></i> @@ -99,6 +102,7 @@ Configure OPNFV </a> </div> + {% endblock dropDown %} <a href="{% url 'resource:hosts' %}" class="list-group-item list-group-item-action"> Hosts </a> diff --git a/src/templates/base/booking/quick_deploy.html b/src/templates/base/booking/quick_deploy.html index 8570f25..42148c8 100644 --- a/src/templates/base/booking/quick_deploy.html +++ b/src/templates/base/booking/quick_deploy.html @@ -12,7 +12,7 @@ {% bootstrap_field form.filter_field show_label=False %} </div> </div> - <div class="row"> + <div class="row justify-content-center"> <div class="col-12 col-lg-3 my-2"> <div class="col border rounded py-2 h-100"> {% bootstrap_field form.purpose %} @@ -25,18 +25,21 @@ </script> </div> </div> + {% block collab %} <div class="col-12 col-lg-3 my-2"> <div class="col border rounded py-2 h-100"> <label>Collaborators</label> {{ form.users }} </div> </div> + {% endblock collab %} <div class="col-12 col-lg-3 my-2"> <div class="col border rounded py-2 h-100"> {% bootstrap_field form.hostname %} {% bootstrap_field form.image %} </div> </div> + {% block opnfv %} <div class="col-12 col-lg-3 my-2"> <div class="col border rounded py-2 h-100"> <strong>OPNFV: (Optional)</strong> @@ -44,6 +47,7 @@ {% bootstrap_field form.scenario %} </div> </div> + {% endblock opnfv %} <div class="col-12 d-flex mt-2 justify-content-end"> <button id="quick_booking_confirm" onclick="submit_form();" type="button" class="btn btn-success">Confirm</button> </div> diff --git a/src/templates/base/dashboard/landing.html b/src/templates/base/dashboard/landing.html index 9e45b09..dd09dc4 100644 --- a/src/templates/base/dashboard/landing.html +++ b/src/templates/base/dashboard/landing.html @@ -32,9 +32,9 @@ To get started, please log in with your <a href="/accounts/login">Linux Foundation Jira account</a> </h4> {% else %} + {% block btnGrp %} <p>To get started, book a server below:</p> - <a class="btn btn-primary btn-lg d-flex flex-column justify-content-center align-content-center border text-white p-4" - href="/booking/quick/"> + <a class="btn btn-primary btn-lg d-flex flex-column justify-content-center align-content-center border text-white p-4" href="/booking/quick/"> Book a Server </a> <p class="mt-4">PTLs can use our advanced options to book multi-node pods. If you are a PTL, you may use the options @@ -45,11 +45,13 @@ <button class="btn btn-primary" onclick="create_workflow(1)">Design a Pod</button> <button class="btn btn-primary" onclick="create_workflow(2)">Configure a Pod</button> </div> + {% endblock btnGrp %} {% endif %} </div> <!-- Returning users --> {% if not request.user.is_anonymous %} + {% block returningUsers %} <div class="col-12 col-lg-6 offset-lg-6 mb-4 mt-lg-4"> <h2 class="ht-4 border-bottom">Returning Users</h2> <p>If you're a returning user, some of the following options may be of interest:</p> @@ -65,6 +67,7 @@ {% endif %} </div> </div> + {% endblock returningUsers %} {% endif %} </div> diff --git a/src/templates/base/layout.html b/src/templates/base/layout.html index f35f1a7..edf9b6b 100644 --- a/src/templates/base/layout.html +++ b/src/templates/base/layout.html @@ -10,7 +10,9 @@ <meta name="description" content=""> <meta name="author" content=""> + {% block head-title %} <title>OPNFV Laas {{ title }}</title> + {% endblock head-title %} <!-- Bootstrap Core CSS --> <link href="{% static "node_modules/bootstrap/dist/css/bootstrap.min.css" %}" diff --git a/src/templates/laas/base.html b/src/templates/laas/base.html new file mode 100644 index 0000000..5e9e9db --- /dev/null +++ b/src/templates/laas/base.html @@ -0,0 +1,13 @@ +{% extends "base/base.html" %} +{% load staticfiles %} +{% block logo %} +<div class="col-12 col-sm order-1 order-sm-2 text-center text-lg-left"> + <a href="https://www.opnfv.org/" class="navbar-brand"> + <img src="{% static "img/opnfv-logo.png" %}"> + </a> + <a class="navbar-brand d-none d-lg-inline" href={% url 'dashboard:index' %}> + Laas Dashboard + </a> +</div> +{% endblock logo %} + diff --git a/src/templates/laas/layout.html b/src/templates/laas/layout.html new file mode 100644 index 0000000..f9b1d99 --- /dev/null +++ b/src/templates/laas/layout.html @@ -0,0 +1,5 @@ +{% extends "base/layout.html" %} + +{% block head-title %} +<title>LaaS Dashboard</title> +{% endblock head-title %} 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 |