diff options
Diffstat (limited to 'src/resource_inventory')
-rw-r--r-- | src/resource_inventory/admin.py | 49 | ||||
-rw-r--r-- | src/resource_inventory/migrations/0012_auto_20200103_1850.py | 37 | ||||
-rw-r--r-- | src/resource_inventory/migrations/0012_manual_20200218_1536.py | 25 | ||||
-rw-r--r-- | src/resource_inventory/migrations/0013_auto_20200218_1536.py | 404 | ||||
-rw-r--r-- | src/resource_inventory/models.py | 468 | ||||
-rw-r--r-- | src/resource_inventory/pdf_templater.py | 6 | ||||
-rw-r--r-- | src/resource_inventory/resource_manager.py | 126 | ||||
-rw-r--r-- | src/resource_inventory/views.py | 6 |
8 files changed, 794 insertions, 327 deletions
diff --git a/src/resource_inventory/admin.py b/src/resource_inventory/admin.py index 7ff510b..13afd99 100644 --- a/src/resource_inventory/admin.py +++ b/src/resource_inventory/admin.py @@ -11,16 +11,15 @@ from django.contrib import admin from resource_inventory.models import ( - HostProfile, + ResourceProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile, - GenericResourceBundle, - GenericResource, - GenericHost, - GenericInterface, - Host, + ResourceTemplate, + ResourceConfiguration, + InterfaceConfiguration, + Server, Interface, Network, Vlan, @@ -28,26 +27,30 @@ from resource_inventory.models import ( Scenario, Installer, Opsys, - ConfigBundle, OPNFVConfig, OPNFVRole, Image, - HostConfiguration, RemoteInfo ) -profiles = [HostProfile, InterfaceProfile, DiskProfile, CpuProfile, RamProfile] - -admin.site.register(profiles) - -generics = [GenericResourceBundle, GenericResource, GenericHost, GenericInterface] - -admin.site.register(generics) - -physical = [Host, Interface, Network, Vlan, ResourceBundle] - -admin.site.register(physical) - -config = [Scenario, Installer, Opsys, ConfigBundle, OPNFVConfig, OPNFVRole, Image, HostConfiguration, RemoteInfo] - -admin.site.register(config) +admin.site.register([ + ResourceProfile, + InterfaceProfile, + DiskProfile, + CpuProfile, + RamProfile, + ResourceTemplate, + ResourceConfiguration, + InterfaceConfiguration, + Server, + Interface, + Network, + Vlan, + ResourceBundle, + Scenario, + Installer, + Opsys, + OPNFVConfig, + OPNFVRole, + Image, + RemoteInfo]) diff --git a/src/resource_inventory/migrations/0012_auto_20200103_1850.py b/src/resource_inventory/migrations/0012_auto_20200103_1850.py index 2bb203e..569e433 100644 --- a/src/resource_inventory/migrations/0012_auto_20200103_1850.py +++ b/src/resource_inventory/migrations/0012_auto_20200103_1850.py @@ -4,30 +4,23 @@ from django.db import migrations, models import django.db.models.deletion -def genTempVlanNetwork(apps, editor): +def pairVlanPhysicalNetworks(apps, editor): + PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork") Vlan = apps.get_model("resource_inventory", "Vlan") - Network = apps.get_model("resource_inventory", "Network") - tempVlanNetwork = apps.get_model("resource_inventory", "tempVlanNetwork") for vlan in Vlan.objects.filter(network__isnull=False): - tempVlanNetwork.objects.create(network=vlan.network, vlan=vlan) + if PhysicalNetwork.objects.filter(id=vlan.network.id).exists(): + continue + PhysicalNetwork.objects.create(id=vlan.network.id, vlan_id=vlan.vlan_id, generic_network=vlan.network) -def deleteTempVlanNetworks(apps, editor): - tempVlanNetwork = apps.get_model("resource_inventory", "tempVlanNetwork") - tempVlanNetwork.objects.all().delete() - - -def pairVlanPhysicalNetworks(apps, editor): - PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork") - tempVlanPair = apps.get_model("resource_inventory", "tempVlanNetwork") - for pair in tempVlanPair.objects.all(): - physicalNetwork = PhysicalNetwork.objects.create(vlan_id=vlan.vlan_id, - generic_network=pair.network) - pair.vlan.network = physicalNetwork def deletePhysicalNetworks(apps, editor): + Vlan = apps.get_model("resource_inventory", "Vlan") + for vlan in Vlan.objects.all(): + vlan.network = None PhysicalNetwork = apps.get_model("resource_inventory", "PhysicalNetwork") PhysicalNetwork.objects.all().delete() + class Migration(migrations.Migration): dependencies = [ @@ -56,21 +49,11 @@ class Migration(migrations.Migration): name='id', field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), - migrations.CreateModel( - name='tempVlanNetwork', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('vlan', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.vlan')), - ('network', models.ForeignKey(null=True, to='resource_inventory.network', on_delete=django.db.models.deletion.CASCADE)), - ] - ), - migrations.RunPython(genTempVlanNetwork, deleteTempVlanNetworks), + migrations.RunPython(pairVlanPhysicalNetworks, deletePhysicalNetworks), migrations.AlterField( model_name='vlan', name='network', field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='resource_inventory.PhysicalNetwork', null=True), ), - migrations.RunPython(pairVlanPhysicalNetworks, deletePhysicalNetworks), - migrations.DeleteModel("tempVlanNetwork") ] diff --git a/src/resource_inventory/migrations/0012_manual_20200218_1536.py b/src/resource_inventory/migrations/0012_manual_20200218_1536.py new file mode 100644 index 0000000..378bdc3 --- /dev/null +++ b/src/resource_inventory/migrations/0012_manual_20200218_1536.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2020-02-18 15:36 + +from django.conf import settings +from django.db import migrations + + +def clear_networks(apps, schema_editor): + Network = apps.get_model('resource_inventory', 'Network') + Vlan = apps.get_model('resource_inventory', 'Vlan') + for vlan in Vlan.objects.all(): + vlan.delete() + for net in Network.objects.all(): + net.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('resource_inventory', '0012_auto_20200103_1850'), + ] + + operations = [ + migrations.RunPython(clear_networks) + ] diff --git a/src/resource_inventory/migrations/0013_auto_20200218_1536.py b/src/resource_inventory/migrations/0013_auto_20200218_1536.py new file mode 100644 index 0000000..014cb2f --- /dev/null +++ b/src/resource_inventory/migrations/0013_auto_20200218_1536.py @@ -0,0 +1,404 @@ +# Generated by Django 2.2 on 2020-02-18 15:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import resource_inventory.models + + +def clear_resource_bundles(apps, schema_editor): + ResourceBundle = apps.get_model('resource_inventory', 'ResourceBundle') + for rb in ResourceBundle.objects.all(): + rb.template = None + rb.save() + + +def create_default_template(apps, schema_editor): + ResourceTemplate = apps.get_model('resource_inventory', 'ResourceTemplate') + ResourceTemplate.objects.create(id=1, name="Default Template") + + +def populate_servers(apps, schema_editor): + """Convert old Host models to Server Resources.""" + Host = apps.get_model('resource_inventory', 'Host') + Server = apps.get_model('resource_inventory', 'Server') + ResourceProfile = apps.get_model('resource_inventory', 'ResourceProfile') + for h in Host.objects.all(): + rp = ResourceProfile.objects.get(id=h.profile.id) + Server.objects.create( + working=h.working, + vendor=h.vendor, + labid=h.labid, + booked=h.booked, + name=h.labid, + lab=h.lab, + profile=rp + ) + + +def populate_resource_templates(apps, schema_editor): + """ + Convert old GenericResourceBundles to ResourceTemplate. + + This will be kept blank for now. If, during testing, we realize + we want to implement this, we will. For now, it seems + fine to let the old models just die and create + new ones as needed. + """ + pass + + +def populate_resource_profiles(apps, schema_editor): + """ + Convert old HostProfile models to ResourceProfiles. + + Also updates all the foreign keys pointed to the old + host profile. This change was basically only a name change. + """ + HostProfile = apps.get_model('resource_inventory', 'HostProfile') + ResourceProfile = apps.get_model('resource_inventory', 'ResourceProfile') + for hp in HostProfile.objects.all(): + rp = ResourceProfile.objects.create(id=hp.id, name=hp.name, description=hp.description) + rp.labs.add(*list(hp.labs.all())) + """ + TODO: link these models together + rp.interfaceprofile = hp.interfaceprofile + rp.storageprofile = hp.storageprofile + rp.cpuprofile = hp.cpuprofile + rp.ramprofile = hp.ramprofile + rp.save() + hp.interfaceprofile.host = rp + rp.storageprofile.host = rp + rp.cpuprofile.host = rp + rp.ramprofile.host = rp + rp.interfaceprofile.save() + rp.storageprofile.save() + rp.cpuprofile.save() + rp.ramprofile.save() + """ + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('booking', '0007_remove_booking_config_bundle'), + ('account', '0004_downtime'), + ('api', '0013_manual_20200218_1536'), + ('resource_inventory', '0012_manual_20200218_1536'), + ] + + operations = [ + migrations.CreateModel( + name='InterfaceConfiguration', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('connections', models.ManyToManyField(to='resource_inventory.NetworkConnection')), + ], + ), + migrations.CreateModel( + name='ResourceConfiguration', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('is_head_node', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='ResourceOPNFVConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='ResourceProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=200, unique=True)), + ('description', models.TextField()), + ('labs', models.ManyToManyField(related_name='resourceprofiles', to='account.Lab')), + ], + ), + migrations.RunPython(populate_resource_profiles), + migrations.CreateModel( + name='ResourceTemplate', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=300, unique=True)), + ('xml', models.TextField()), + ('description', models.CharField(default='', max_length=1000)), + ('public', models.BooleanField(default=False)), + ('hidden', models.BooleanField(default=False)), + ('lab', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resourcetemplates', to='account.Lab')), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RunPython(populate_resource_templates), + migrations.CreateModel( + name='Server', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('working', models.BooleanField(default=True)), + ('vendor', models.CharField(default='unknown', max_length=100)), + ('model', models.CharField(default='unknown', max_length=150)), + ('labid', models.CharField(default='default_id', max_length=200, unique=True)), + ('booked', models.BooleanField(default=False)), + ('name', models.CharField(max_length=200, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='server', + name='bundle', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceBundle'), + ), + migrations.AddField( + model_name='server', + name='config', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceConfiguration'), + ), + migrations.AddField( + model_name='server', + name='interfaces', + field=models.ManyToManyField(to='resource_inventory.Interface'), + ), + migrations.AddField( + model_name='server', + name='lab', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab'), + ), + migrations.AddField( + model_name='server', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'), + ), + migrations.AddField( + model_name='server', + name='remote_management', + field=models.ForeignKey(default=resource_inventory.models.get_default_remote_info, on_delete=models.SET(resource_inventory.models.get_default_remote_info), to='resource_inventory.RemoteInfo'), + ), + migrations.RunPython(populate_servers), + migrations.RemoveField( + model_name='generichost', + name='profile', + ), + migrations.RemoveField( + model_name='generichost', + name='resource', + ), + migrations.RemoveField( + model_name='genericinterface', + name='connections', + ), + migrations.RemoveField( + model_name='genericinterface', + name='host', + ), + migrations.RemoveField( + model_name='genericinterface', + name='profile', + ), + migrations.RemoveField( + model_name='genericresource', + name='bundle', + ), + migrations.RemoveField( + model_name='genericresourcebundle', + name='lab', + ), + migrations.RemoveField( + model_name='genericresourcebundle', + name='owner', + ), + migrations.RemoveField( + model_name='host', + name='bundle', + ), + migrations.RemoveField( + model_name='host', + name='config', + ), + migrations.RemoveField( + model_name='host', + name='lab', + ), + migrations.RemoveField( + model_name='host', + name='profile', + ), + migrations.RemoveField( + model_name='host', + name='remote_management', + ), + migrations.RemoveField( + model_name='host', + name='template', + ), + migrations.RemoveField( + model_name='hostconfiguration', + name='bundle', + ), + migrations.RemoveField( + model_name='hostconfiguration', + name='host', + ), + migrations.RemoveField( + model_name='hostconfiguration', + name='image', + ), + migrations.RemoveField( + model_name='hostopnfvconfig', + name='host_config', + ), + migrations.RemoveField( + model_name='hostopnfvconfig', + name='opnfv_config', + ), + migrations.RemoveField( + model_name='hostopnfvconfig', + name='role', + ), + migrations.RemoveField( + model_name='hostprofile', + name='labs', + ), + migrations.RemoveField( + model_name='interface', + name='host', + ), + migrations.RemoveField( + model_name='interface', + name='name', + ), + migrations.RemoveField( + model_name='opnfvconfig', + name='bundle', + ), + migrations.AddField( + model_name='interface', + name='profile', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'), + preserve_default=False, + ), + migrations.AddField( + model_name='interfaceprofile', + name='order', + field=models.IntegerField(default=-1), + ), + migrations.AlterField( + model_name='cpuprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpuprofile', to='resource_inventory.ResourceProfile'), + ), + migrations.AlterField( + model_name='diskprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='storageprofile', to='resource_inventory.ResourceProfile'), + ), + migrations.AlterField( + model_name='image', + name='host_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'), + ), + migrations.AlterField( + model_name='interfaceprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interfaceprofile', to='resource_inventory.ResourceProfile'), + ), + migrations.AlterField( + model_name='network', + name='bundle', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='networks', to='resource_inventory.ResourceTemplate'), + ), + migrations.AlterField( + model_name='ramprofile', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ramprofile', to='resource_inventory.ResourceProfile'), + ), + migrations.RunPython(clear_resource_bundles), + migrations.AlterField( + model_name='resourcebundle', + name='template', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.ResourceTemplate'), + ), + migrations.DeleteModel( + name='ConfigBundle', + ), + migrations.DeleteModel( + name='GenericHost', + ), + migrations.DeleteModel( + name='GenericInterface', + ), + migrations.DeleteModel( + name='GenericResource', + ), + migrations.DeleteModel( + name='GenericResourceBundle', + ), + migrations.DeleteModel( + name='HostConfiguration', + ), + migrations.DeleteModel( + name='HostOPNFVConfig', + ), + migrations.DeleteModel( + name='HostProfile', + ), + migrations.DeleteModel( + name='Host', + ), + migrations.AddField( + model_name='resourceopnfvconfig', + name='opnfv_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_config', to='resource_inventory.OPNFVConfig'), + ), + migrations.AddField( + model_name='resourceopnfvconfig', + name='resource_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_config', to='resource_inventory.ResourceConfiguration'), + ), + migrations.AddField( + model_name='resourceopnfvconfig', + name='role', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resource_opnfv_configs', to='resource_inventory.OPNFVRole'), + ), + migrations.AddField( + model_name='resourceconfiguration', + name='image', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='resource_inventory.Image'), + ), + migrations.AddField( + model_name='resourceconfiguration', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.ResourceProfile'), + ), + migrations.AddField( + model_name='resourceconfiguration', + name='template', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='resourceConfigurations', to='resource_inventory.ResourceTemplate'), + ), + migrations.AddField( + model_name='interfaceconfiguration', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resource_inventory.InterfaceProfile'), + ), + migrations.AddField( + model_name='interfaceconfiguration', + name='resource_config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interface_configs', to='resource_inventory.ResourceConfiguration'), + ), + migrations.AddField( + model_name='interface', + name='acts_as', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='resource_inventory.InterfaceConfiguration'), + ), + migrations.RunPython(create_default_template), + migrations.AddField( + model_name='opnfvconfig', + name='template', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='opnfv_config', to='resource_inventory.ResourceTemplate'), + preserve_default=False, + ), + ] diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index a8b75d9..d11f71b 100644 --- a/src/resource_inventory/models.py +++ b/src/resource_inventory/models.py @@ -8,21 +8,30 @@ ############################################################################## from django.contrib.auth.models import User +from django.core.exceptions import ValidationError from django.db import models -from django.core.validators import RegexValidator +from django.db.models import Q import re from account.models import Lab +from dashboard.utils import AbstractModelQuery -# profile of resources hosted by labs -class HostProfile(models.Model): +""" +Profiles of resources hosted by labs. + +These describe hardware attributes of the different Resources a lab hosts. +A single Resource subclass (e.g. Server) may have instances that point to different +Profile models (e.g. an x86 server profile and armv8 server profile. +""" + + +class ResourceProfile(models.Model): id = models.AutoField(primary_key=True) - host_type = models.PositiveSmallIntegerField(default=0) name = models.CharField(max_length=200, unique=True) description = models.TextField() - labs = models.ManyToManyField(Lab, related_name="hostprofiles") + labs = models.ManyToManyField(Lab, related_name="resourceprofiles") def validate(self): validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$") @@ -34,12 +43,33 @@ class HostProfile(models.Model): def __str__(self): return self.name + def get_resources(self, lab=None, working=True, unreserved=False): + """ + Return a list of Resource objects which have this profile. + + If lab is provided, only resources at that lab will be returned. + If working=True, will only return working hosts + """ + resources = [] + query = Q(profile=self) + if lab: + query = query & Q(lab=lab) + if working: + query = query & Q(working=True) + + resources = ResourceQuery.filter(query) + + if unreserved: + resources = [r for r in resources if not r.is_reserved()] + + return resources + class InterfaceProfile(models.Model): id = models.AutoField(primary_key=True) speed = models.IntegerField() name = models.CharField(max_length=100) - host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='interfaceprofile') + host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile') nic_type = models.CharField( max_length=50, choices=[ @@ -48,6 +78,7 @@ class InterfaceProfile(models.Model): ], default="onboard" ) + order = models.IntegerField(default=-1) def __str__(self): return self.name + " for " + str(self.host) @@ -61,7 +92,7 @@ class DiskProfile(models.Model): ("HDD", "HDD") ]) name = models.CharField(max_length=50) - host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='storageprofile') + host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile') rotation = models.IntegerField(default=0) interface = models.CharField( max_length=50, @@ -88,7 +119,7 @@ class CpuProfile(models.Model): ("aarch64", "aarch64") ]) cpus = models.IntegerField() - host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='cpuprofile') + host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile') cflags = models.TextField(null=True) def __str__(self): @@ -99,16 +130,116 @@ class RamProfile(models.Model): id = models.AutoField(primary_key=True) amount = models.IntegerField() channels = models.IntegerField() - host = models.ForeignKey(HostProfile, on_delete=models.CASCADE, related_name='ramprofile') + host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='ramprofile') def __str__(self): return str(self.amount) + "G for " + str(self.host) +""" +Resource Models + +These models represent actual hardware resources +with varying degrees of abstraction. +""" + + +class ResourceTemplate(models.Model): + """ + Models a "template" of a complete, configured collection of resources that can be booked. + + For example, this may represent a Pharos POD. This model is a template of the actual + resources that will be booked. This model can be "instantiated" into real resource models + across multiple different bookings. + """ + + # TODO: template might not be a good name because this is a collection of lots of configured resources + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=300, unique=True) + xml = models.TextField() + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL, related_name="resourcetemplates") + description = models.CharField(max_length=1000, default="") + public = models.BooleanField(default=False) + hidden = models.BooleanField(default=False) + + def getConfigs(self): + return list(self.resourceConfigurations.all()) + + def __str__(self): + return self.name + + +class ResourceBundle(models.Model): + """ + Collection of Resource objects. + + This is just a way of aggregating all the resources in a booking into a single model. + """ + + template = models.ForeignKey(ResourceTemplate, on_delete=models.SET_NULL, null=True) + + def __str__(self): + if self.template is None: + return "Resource bundle " + str(self.id) + " with no template" + return "instance of " + str(self.template) + + def get_resources(self): + return ResourceQuery.filter(bundle=self) + + def get_resource_with_role(self, role): + # TODO + pass + + +class ResourceConfiguration(models.Model): + """Model to represent a complete configuration for a single physical Resource.""" + + id = models.AutoField(primary_key=True) + profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) + image = models.ForeignKey("Image", on_delete=models.PROTECT) + template = models.ForeignKey(ResourceTemplate, related_name="resourceConfigurations", null=True, on_delete=models.CASCADE) + is_head_node = models.BooleanField(default=False) + # name? + + def __str__(self): + return "config with " + str(self.template) + " and image " + str(self.image) + + +def get_default_remote_info(): + return RemoteInfo.objects.get_or_create( + address="default", + mac_address="default", + password="default", + user="default", + management_type="default", + versions="[default]" + )[0].pk + + class Resource(models.Model): + """ + Super class for all hardware resource models. + + Defines methods that must be implemented and common database fields. + Any new kind of Resource a lab wants to host (White box switch, traffic generator, etc) + should inherit from this class and fulfill the functional interface + """ + class Meta: abstract = True + bundle = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, null=True) + profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) + config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, null=True) + working = models.BooleanField(default=True) + vendor = models.CharField(max_length=100, default="unknown") + model = models.CharField(max_length=150, default="unknown") + interfaces = models.ManyToManyField("Interface") + remote_management = models.ForeignKey("RemoteInfo", default=get_default_remote_info, on_delete=models.SET(get_default_remote_info)) + labid = models.CharField(max_length=200, default="default_id", unique=True) + lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + def get_configuration(self, state): """ Get configuration of Resource. @@ -129,45 +260,129 @@ class Resource(models.Model): def get_interfaces(self): """ - Returns a list of interfaces on this resource. + Return a list of interfaces on this resource. + The ordering of interfaces should be consistent. """ raise NotImplementedError("Must implement in concrete Resource classes") + def is_reserved(self): + """Return True if this Resource is reserved.""" + raise NotImplementedError("Must implement in concrete Resource classes") + + def same_instance(self, other): + """Return True if this Resource is the same instance as other.""" + raise NotImplementedError("Must implement in concrete Resource classes") + + def save(self, *args, **kwargs): + """Assert that labid is unique across all Resource models.""" + res = ResourceQuery.filter(labid=self.labid) + if len(res) > 1: + raise ValidationError("Too many resources with labid " + str(self.labid)) + + if len(res) == 1: + if not self.same_instance(res[0]): + raise ValidationError("Too many resources with labid " + str(self.labid)) + super().save(*args, **kwargs) -# Generic resource templates -class GenericResourceBundle(models.Model): + +class RemoteInfo(models.Model): + address = models.CharField(max_length=15) + mac_address = models.CharField(max_length=17) + password = models.CharField(max_length=100) + user = models.CharField(max_length=100) + management_type = models.CharField(max_length=50, default="ipmi") + versions = models.CharField(max_length=100) # json serialized list of floats + + +class Server(Resource): + """Resource subclass - a basic baremetal server.""" + + booked = models.BooleanField(default=False) + name = models.CharField(max_length=200, unique=True) + + def __str__(self): + return self.name + + def get_configuration(self, state): + ipmi = state == ConfigState.NEW + power = "off" if state == ConfigState.CLEAN else "on" + + return { + "id": self.labid, + "image": self.config.image.lab_id, + "hostname": self.template.resource.name, + "power": power, + "ipmi_create": str(ipmi) + } + + def get_interfaces(self): + return list(self.interfaces.all().order_by('bus_address')) + + def release(self): + self.booked = False + self.save() + + def reserve(self): + self.booked = True + self.save() + + def is_reserved(self): + return self.booked + + def same_instance(self, other): + return isinstance(other, Server) and other.name == self.name + + +class Opsys(models.Model): id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300, unique=True) - xml = models.TextField() - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL) - description = models.CharField(max_length=1000, default="") - public = models.BooleanField(default=False) - hidden = models.BooleanField(default=False) + name = models.CharField(max_length=100) + sup_installers = models.ManyToManyField("Installer", blank=True) + + def __str__(self): + return self.name - def getResources(self): - my_resources = [] - for genericResource in self.generic_resources.all(): - my_resources.append(genericResource.getResource()) - return my_resources +class Image(models.Model): + """Model for representing OS images / snapshots of hosts.""" + + id = models.AutoField(primary_key=True) + lab_id = models.IntegerField() # ID the lab who holds this image knows + from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) + name = models.CharField(max_length=200) + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + public = models.BooleanField(default=True) + host_type = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) + description = models.TextField() + os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) def __str__(self): return self.name + def in_use(self): + for resource in ResourceQuery.filter(config__image=self): + if resource.is_reserved(): + return True + + return False + + +""" +Networking configuration models +""" + class Network(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) - bundle = models.ForeignKey(GenericResourceBundle, on_delete=models.CASCADE, related_name="networks") + bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks") is_public = models.BooleanField() def __str__(self): return self.name -class PhysicalNetwork(Resource): +class PhysicalNetwork(models.Model): vlan_id = models.IntegerField() generic_network = models.ForeignKey(Network, on_delete=models.CASCADE) @@ -205,65 +420,21 @@ class Vlan(models.Model): return str(self.vlan_id) + ("_T" if self.tagged else "") -class ConfigState: - NEW = 0 - RESET = 100 - CLEAN = 200 - - -class GenericResource(models.Model): - bundle = models.ForeignKey(GenericResourceBundle, related_name='generic_resources', on_delete=models.CASCADE) - hostname_validchars = RegexValidator(regex=r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))', message="Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)") - name = models.CharField(max_length=200, validators=[hostname_validchars]) - - def getResource(self): - # TODO: This will have to be dealt with - return self.generic_host - - def __str__(self): - return self.name - - def validate(self): - validname = re.compile(r'(?=^.{1,253}$)(?=(^([A-Za-z0-9\-\_]{1,62}\.)*[A-Za-z0-9\-\_]{1,63}$))') - if not validname.match(self.name): - return "Enter a valid hostname. Full domain name may be 1-253 characters, each hostname 1-63 characters (including suffixed dot), and valid characters for hostnames are A-Z, a-z, 0-9, hyphen (-), and underscore (_)" - else: - return None - - -# Host template -class GenericHost(models.Model): - id = models.AutoField(primary_key=True) - profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE) - resource = models.OneToOneField(GenericResource, related_name='generic_host', on_delete=models.CASCADE) - - def __str__(self): - return self.resource.name - - -# Physical, actual resources -class ResourceBundle(Resource): - template = models.ForeignKey(GenericResourceBundle, on_delete=models.SET_NULL, null=True) - - def __str__(self): - if self.template is None: - return "Resource bundle " + str(self.id) + " with no template" - return "instance of " + str(self.template) - - def get_host(self, role="Jumphost"): - return Host.objects.filter(bundle=self, config__is_head_node=True).first() # should only ever be one, but it is not an invariant in the models - - -class GenericInterface(models.Model): +class InterfaceConfiguration(models.Model): id = models.AutoField(primary_key=True) profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) - host = models.ForeignKey(GenericHost, on_delete=models.CASCADE, related_name='generic_interfaces') + resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs') connections = models.ManyToManyField(NetworkConnection) def __str__(self): return "type " + str(self.profile) + " on host " + str(self.host) +""" +OPNFV / Software configuration models +""" + + class Scenario(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=300) @@ -281,38 +452,16 @@ class Installer(models.Model): return self.name -class Opsys(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - sup_installers = models.ManyToManyField(Installer, blank=True) - - def __str__(self): - return self.name - - class NetworkRole(models.Model): name = models.CharField(max_length=100) network = models.ForeignKey(Network, on_delete=models.CASCADE) -class ConfigBundle(models.Model): - id = models.AutoField(primary_key=True) - owner = models.ForeignKey(User, on_delete=models.CASCADE) - name = models.CharField(max_length=200, unique=True) - description = models.CharField(max_length=1000, default="") - bundle = models.ForeignKey(GenericResourceBundle, null=True, on_delete=models.CASCADE) - public = models.BooleanField(default=False) - hidden = models.BooleanField(default=False) - - def __str__(self): - return self.name - - class OPNFVConfig(models.Model): id = models.AutoField(primary_key=True) installer = models.ForeignKey(Installer, on_delete=models.CASCADE) scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE) - bundle = models.ForeignKey(ConfigBundle, related_name="opnfv_config", on_delete=models.CASCADE) + template = models.ForeignKey(ResourceTemplate, related_name="opnfv_config", on_delete=models.CASCADE) networks = models.ManyToManyField(NetworkRole) name = models.CharField(max_length=300, blank=True, default="") description = models.CharField(max_length=600, blank=True, default="") @@ -330,105 +479,14 @@ class OPNFVRole(models.Model): return self.name -class Image(models.Model): - """Model for representing OS images / snapshots of hosts.""" - - id = models.AutoField(primary_key=True) - lab_id = models.IntegerField() # ID the lab who holds this image knows - from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - name = models.CharField(max_length=200) - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - public = models.BooleanField(default=True) - host_type = models.ForeignKey(HostProfile, on_delete=models.CASCADE) - description = models.TextField() - os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) - - def __str__(self): - return self.name - - def in_use(self): - return Host.objects.filter(booked=True, config__image=self).exists() - - def get_sentinal_opnfv_role(): return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.") -class HostConfiguration(models.Model): - """Model to represent a complete configuration for a single physical host.""" - - id = models.AutoField(primary_key=True) - host = models.ForeignKey(GenericHost, related_name="configuration", on_delete=models.CASCADE) - image = models.ForeignKey(Image, on_delete=models.PROTECT) - bundle = models.ForeignKey(ConfigBundle, related_name="hostConfigurations", null=True, on_delete=models.CASCADE) - is_head_node = models.BooleanField(default=False) - - def __str__(self): - return "config with " + str(self.host) + " and image " + str(self.image) - - -class HostOPNFVConfig(models.Model): - role = models.ForeignKey(OPNFVRole, related_name="host_opnfv_configs", on_delete=models.CASCADE) - host_config = models.ForeignKey(HostConfiguration, related_name="host_opnfv_config", on_delete=models.CASCADE) - opnfv_config = models.ForeignKey(OPNFVConfig, related_name="host_opnfv_config", on_delete=models.CASCADE) - - -class RemoteInfo(models.Model): - address = models.CharField(max_length=15) - mac_address = models.CharField(max_length=17) - password = models.CharField(max_length=100) - user = models.CharField(max_length=100) - management_type = models.CharField(max_length=50, default="ipmi") - versions = models.CharField(max_length=100) # json serialized list of floats - - -def get_default_remote_info(): - return RemoteInfo.objects.get_or_create( - address="default", - mac_address="default", - password="default", - user="default", - management_type="default", - versions="[default]" - )[0].pk - - -# Concrete host, actual machine in a lab -class Host(Resource): - template = models.ForeignKey(GenericHost, on_delete=models.SET_NULL, null=True) - booked = models.BooleanField(default=False) - name = models.CharField(max_length=200, unique=True) - bundle = models.ForeignKey(ResourceBundle, related_name='hosts', on_delete=models.SET_NULL, null=True) - config = models.ForeignKey(HostConfiguration, null=True, related_name="configuration", on_delete=models.SET_NULL) - labid = models.CharField(max_length=200, default="default_id") - profile = models.ForeignKey(HostProfile, on_delete=models.CASCADE) - lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - working = models.BooleanField(default=True) - vendor = models.CharField(max_length=100, default="unknown") - model = models.CharField(max_length=150, default="unknown") - remote_management = models.ForeignKey(RemoteInfo, default=get_default_remote_info, on_delete=models.SET(get_default_remote_info)) - - def __str__(self): - return self.name - - def get_configuration(self, state): - ipmi = state == ConfigState.NEW - power = "off" if state == ConfigState.CLEAN else "on" - - return { - "id": self.labid, - "image": self.config.image.lab_id, - "hostname": self.template.resource.name, - "power": power, - "ipmi_create": str(ipmi) - } - - def release(self): - self.booked = False - self.save() - - def get_interfaces(self): - return list(self.interfaces.all().order_by('bus_address')) +class ResourceOPNFVConfig(models.Model): + role = models.ForeignKey(OPNFVRole, related_name="resource_opnfv_configs", on_delete=models.CASCADE) + resource_config = models.ForeignKey(ResourceConfiguration, related_name="resource_opnfv_config", on_delete=models.CASCADE) + opnfv_config = models.ForeignKey(OPNFVConfig, related_name="resource_opnfv_config", on_delete=models.CASCADE) class Interface(models.Model): @@ -436,15 +494,33 @@ class Interface(models.Model): mac_address = models.CharField(max_length=17) bus_address = models.CharField(max_length=50) config = models.ManyToManyField(Vlan) - host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='interfaces') + acts_as = models.OneToOneField(InterfaceConfiguration, null=True, on_delete=models.SET_NULL) profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) def __str__(self): return self.mac_address + " on host " + str(self.host) +""" +Some Enums for dealing with global constants. +""" + + class OPNFV_SETTINGS(): """This is a static configuration class.""" # all the required network types in PDF/IDF spec NETWORK_ROLES = ["public", "private", "admin", "mgmt"] + + +class ConfigState: + NEW = 0 + RESET = 100 + CLEAN = 200 + + +RESOURCE_TYPES = [Server] + + +class ResourceQuery(AbstractModelQuery): + model_list = [Server] diff --git a/src/resource_inventory/pdf_templater.py b/src/resource_inventory/pdf_templater.py index 51e3746..6844b09 100644 --- a/src/resource_inventory/pdf_templater.py +++ b/src/resource_inventory/pdf_templater.py @@ -10,7 +10,7 @@ from django.template.loader import render_to_string import booking -from resource_inventory.models import Host, InterfaceProfile +from resource_inventory.models import Server, InterfaceProfile class PDFTemplater: @@ -66,7 +66,7 @@ class PDFTemplater: ) jumphost = booking.resource.hosts.get(config=jumphost_opnfv_config.host_config) else: # if there is no opnfv config, use headnode - jumphost = Host.objects.filter( + jumphost = Server.objects.filter( bundle=booking.resource, config__is_head_node=True ).first() @@ -85,7 +85,7 @@ class PDFTemplater: def get_pdf_nodes(cls, booking): """Return a list of all the "nodes" (every host except jumphost).""" pdf_nodes = [] - nodes = set(Host.objects.filter(bundle=booking.resource)) + nodes = set(Server.objects.filter(bundle=booking.resource)) nodes.discard(cls.get_jumphost(booking)) for node in nodes: diff --git a/src/resource_inventory/resource_manager.py b/src/resource_inventory/resource_manager.py index 242d21a..c8b2b05 100644 --- a/src/resource_inventory/resource_manager.py +++ b/src/resource_inventory/resource_manager.py @@ -7,18 +7,13 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## import re +from django.db.models import Q + +from dashboard.exceptions import ResourceAvailabilityException -from dashboard.exceptions import ( - ResourceExistenceException, - ResourceAvailabilityException, - ResourceProvisioningException, - ModelValidationException, -) from resource_inventory.models import ( - Host, - HostConfiguration, ResourceBundle, - HostProfile, + ResourceTemplate, Network, Vlan, PhysicalNetwork, @@ -38,32 +33,27 @@ class ResourceManager: ResourceManager.instance = ResourceManager() return ResourceManager.instance - def getAvailableHostTypes(self, lab): - hostset = Host.objects.filter(lab=lab).filter(booked=False).filter(working=True) - hostprofileset = HostProfile.objects.filter(host__in=hostset, labs=lab) - return set(hostprofileset) + def getAvailableResourceTemplates(self, lab, user): + templates = ResourceTemplate.objects.filter(lab=lab) + templates.filter(Q(owner=user) | Q(public=True)) + return templates - def hostsAvailable(self, grb): + def templateIsReservable(self, resource_template): """ - Check if the given GenericResourceBundle is available. + Check if the required resources to reserve this template is available. No changes to the database """ # count up hosts profile_count = {} - for host in grb.getResources(): - if host.profile not in profile_count: - profile_count[host.profile] = 0 - profile_count[host.profile] += 1 + for config in resource_template.getConfigs(): + if config.profile not in profile_count: + profile_count[config.profile] = 0 + profile_count[config.profile] += 1 # check that all required hosts are available for profile in profile_count.keys(): - available = Host.objects.filter( - booked=False, - working=True, - lab=grb.lab, - profile=profile - ).count() + available = len(profile.get_resources(lab=resource_template.lab, unreserved=True)) needed = profile_count[profile] if available < needed: return False @@ -71,8 +61,8 @@ class ResourceManager: # public interface def deleteResourceBundle(self, resourceBundle): - for host in Host.objects.filter(bundle=resourceBundle): - host.release() + for resource in resourceBundle.get_resources(): + resource.release() resourceBundle.delete() def get_vlans(self, genericResourceBundle): @@ -89,43 +79,32 @@ class ResourceManager: networks[network.name] = vlan return networks - def convertResourceBundle(self, genericResourceBundle, config=None): + def instantiateTemplate(self, resource_template, config=None): """ - Convert a GenericResourceBundle into a ResourceBundle. + Convert a ResourceTemplate into a ResourceBundle. - Takes in a genericResourceBundle and reserves all the + Takes in a ResourceTemplate and reserves all the Resources needed and returns a completed ResourceBundle. """ - resource_bundle = ResourceBundle.objects.create(template=genericResourceBundle) - generic_hosts = genericResourceBundle.getResources() - physical_hosts = [] + resource_bundle = ResourceBundle.objects.create(template=resource_template) + res_configs = resource_template.getConfigs() + resources = [] - vlan_map = self.get_vlans(genericResourceBundle) + vlan_map = self.get_vlans(resource_template) - for generic_host in generic_hosts: - host_config = None - if config: - host_config = HostConfiguration.objects.get(bundle=config, host=generic_host) - try: - physical_host = self.acquireHost(generic_host, genericResourceBundle.lab.name) - except ResourceAvailabilityException: - self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle) - raise ResourceAvailabilityException("Could not provision hosts, not enough available") + for config in res_configs: try: - physical_host.bundle = resource_bundle - physical_host.template = generic_host - physical_host.config = host_config - physical_hosts.append(physical_host) - - self.configureNetworking(physical_host, vlan_map) - except Exception: - self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle) - raise ResourceProvisioningException("Network configuration failed.") - try: - physical_host.save() - except Exception: - self.fail_acquire(physical_hosts, vlan_map, genericResourceBundle) - raise ModelValidationException("Saving hosts failed") + phys_res = self.acquireHost(config) + phys_res.bundle = resource_bundle + phys_res.config = config + resources.append(phys_res) + + self.configureNetworking(phys_res, vlan_map) + phys_res.save() + + except Exception as e: + self.fail_acquire(resources, vlan_map, resource_template) + raise e return resource_bundle @@ -149,30 +128,27 @@ class ResourceManager: ) # private interface - def acquireHost(self, genericHost, labName): - host_full_set = Host.objects.filter(lab__name__exact=labName, profile=genericHost.profile) - if not host_full_set.first(): - raise ResourceExistenceException("No matching servers found") - host_set = host_full_set.filter(booked=False, working=True) - if not host_set.first(): - raise ResourceAvailabilityException("No unbooked hosts match requested hosts") - host = host_set.first() - host.booked = True - host.template = genericHost - host.save() - return host - - def releaseNetworks(self, grb, vlan_manager, vlans): + def acquireHost(self, resource_config): + resources = resource_config.profile.get_resources(lab=resource_config.lab, unreserved=True) + try: + resource = resources[0] # TODO: should we randomize and 'load balance' the servers? + resource.config = resource_config + resource.reserve() + return resource + except IndexError: + raise ResourceAvailabilityException("No available resources of requested type") + + def releaseNetworks(self, template, vlans): + vlan_manager = template.lab.vlan_manager for net_name, vlan_id in vlans.items(): - net = Network.objects.get(name=net_name, bundle=grb) + net = Network.objects.get(name=net_name, bundle=template) if(net.is_public): vlan_manager.release_public_vlan(vlan_id) else: vlan_manager.release_vlans(vlan_id) - def fail_acquire(self, hosts, vlans, grb): - vlan_manager = grb.lab.vlan_manager - self.releaseNetworks(grb, vlan_manager, vlans) + def fail_acquire(self, hosts, vlans, template): + self.releaseNetworks(template, vlans) for host in hosts: host.release() diff --git a/src/resource_inventory/views.py b/src/resource_inventory/views.py index 8c3d899..52f8c75 100644 --- a/src/resource_inventory/views.py +++ b/src/resource_inventory/views.py @@ -12,7 +12,7 @@ from django.views.generic import TemplateView from django.shortcuts import get_object_or_404 from django.shortcuts import render -from resource_inventory.models import HostProfile, Host +from resource_inventory.models import ResourceProfile, ResourceQuery class HostView(TemplateView): @@ -20,13 +20,13 @@ class HostView(TemplateView): def get_context_data(self, **kwargs): context = super(HostView, self).get_context_data(**kwargs) - hosts = Host.objects.filter(working=True) + hosts = ResourceQuery.filter(working=True) context.update({'hosts': hosts, 'title': "Hardware Resources"}) return context def hostprofile_detail_view(request, hostprofile_id): - hostprofile = get_object_or_404(HostProfile, id=hostprofile_id) + hostprofile = get_object_or_404(ResourceProfile, id=hostprofile_id) return render( request, |