Modifies Resource Models for ongoing refactor 55/69655/12
authorParker Berberian <pberberian@iol.unh.edu>
Thu, 6 Feb 2020 17:59:51 +0000 (12:59 -0500)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Wed, 12 Feb 2020 18:23:17 +0000 (13:23 -0500)
Change-Id: Ice88f53135f57aca8e2de4d69274e7d490f981a4
Signed-off-by: Parker Berberian <pberberian@iol.unh.edu>
src/api/models.py
src/booking/forms.py
src/booking/models.py
src/booking/quick_deployer.py
src/dashboard/utils.py [new file with mode: 0644]
src/resource_inventory/admin.py
src/resource_inventory/models.py
src/resource_inventory/pdf_templater.py
src/resource_inventory/resource_manager.py
src/workflow/forms.py
src/workflow/models.py

index 1e5a2da..de73a7a 100644 (file)
@@ -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
index 9b4db86..b9c9231 100644 (file)
@@ -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(
index 8f2446f..cf8bf1d 100644 (file)
@@ -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)
index 743cdcf..94ad14d 100644 (file)
 
 
 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 (file)
index 0000000..af2461e
--- /dev/null
@@ -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))
index 7ff510b..ab21dd1 100644 (file)
@@ -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
 )
 
index a8b75d9..20e080b 100644 (file)
@@ -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]
index 51e3746..6844b09 100644 (file)
@@ -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:
index 242d21a..e14218b 100644 (file)
@@ -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()
 
index f7a20eb..37bc390 100644 (file)
@@ -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,
index 32ac39c..4d32869 100644 (file)
@@ -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']