diff options
Diffstat (limited to 'src/resource_inventory/models.py')
-rw-r--r-- | src/resource_inventory/models.py | 468 |
1 files changed, 272 insertions, 196 deletions
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] |