diff options
author | Parker Berberian <pberberian@iol.unh.edu> | 2020-02-06 12:59:51 -0500 |
---|---|---|
committer | Sawyer Bergeron <sbergeron@iol.unh.edu> | 2020-02-12 13:23:17 -0500 |
commit | f5cdab1569b26df0c7ffc3df1529f095116fd13a (patch) | |
tree | 0905a58a36b5be4a38613d1cb5834ec2e4d5e27f | |
parent | 80f9bb0bb514133363bd0a40edb8b10ddb8d3a51 (diff) |
Modifies Resource Models for ongoing refactor
Change-Id: Ice88f53135f57aca8e2de4d69274e7d490f981a4
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
-rw-r--r-- | src/api/models.py | 185 | ||||
-rw-r--r-- | src/booking/forms.py | 2 | ||||
-rw-r--r-- | src/booking/models.py | 4 | ||||
-rw-r--r-- | src/booking/quick_deployer.py | 282 | ||||
-rw-r--r-- | src/dashboard/utils.py | 24 | ||||
-rw-r--r-- | src/resource_inventory/admin.py | 4 | ||||
-rw-r--r-- | src/resource_inventory/models.py | 465 | ||||
-rw-r--r-- | src/resource_inventory/pdf_templater.py | 6 | ||||
-rw-r--r-- | src/resource_inventory/resource_manager.py | 121 | ||||
-rw-r--r-- | src/workflow/forms.py | 37 | ||||
-rw-r--r-- | src/workflow/models.py | 4 |
11 files changed, 526 insertions, 608 deletions
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/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/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..94ad14d 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.form import ValidationException 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, ) 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 ValidationException("No lab was selected") + if template is None: + raise ValidationException("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 HostOPNFVConfig.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 ValidationException("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 ValidationException("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 ValidationException("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 ValidationException("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 ValidationException("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/dashboard/utils.py b/src/dashboard/utils.py new file mode 100644 index 0000000..af2461e --- /dev/null +++ b/src/dashboard/utils.py @@ -0,0 +1,24 @@ + +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)) diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py index 7ff510b..ab21dd1 100644 --- a/src/resource_inventory/admin.py +++ b/src/resource_inventory/admin.py @@ -11,7 +11,7 @@ from django.contrib import admin from resource_inventory.models import ( - HostProfile, + ResourceProfile, InterfaceProfile, DiskProfile, CpuProfile, @@ -32,7 +32,7 @@ from resource_inventory.models import ( OPNFVConfig, OPNFVRole, Image, - HostConfiguration, + ResourceConfiguration, RemoteInfo ) diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index a8b75d9..20e080b 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,115 @@ 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) + + 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,38 +259,122 @@ 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): @@ -205,65 +419,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 +451,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 +478,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 +493,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..e14218b 100644 --- a/src/resource_inventory/resource_manager.py +++ b/src/resource_inventory/resource_manager.py @@ -8,17 +8,10 @@ ############################################################################## import re -from dashboard.exceptions import ( - ResourceExistenceException, - ResourceAvailabilityException, - ResourceProvisioningException, - ModelValidationException, -) +from dashboard.exceptions import ResourceAvailabilityException + from resource_inventory.models import ( - Host, - HostConfiguration, ResourceBundle, - HostProfile, Network, Vlan, PhysicalNetwork, @@ -38,32 +31,22 @@ 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 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 +54,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 +72,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) + for config in res_configs: 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") - 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 +121,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/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..4d32869 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, InterfaceProfile, 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'] |