diff options
author | Justin Choquette <jchoquette@iol.unh.edu> | 2023-06-08 12:46:53 -0400 |
---|---|---|
committer | Justin Choquette <jchoquette@iol.unh.edu> | 2023-07-21 13:17:51 -0400 |
commit | a09db9f287a02873c0226759f8ea444bb304cd59 (patch) | |
tree | 59e744e4b998973a808abbae2d21fbdd6201d829 /src/resource_inventory/models.py | |
parent | 8ddc7e820e120f1dde4e901d3cb6f1dd3f281e65 (diff) |
LaaS 3.0 Almost MVP
Change-Id: Ided9a43cf3088bb58a233dc459711c03f43e11b8
Signed-off-by: Justin Choquette <jchoquette@iol.unh.edu>
Diffstat (limited to 'src/resource_inventory/models.py')
-rw-r--r-- | src/resource_inventory/models.py | 689 |
1 files changed, 11 insertions, 678 deletions
diff --git a/src/resource_inventory/models.py b/src/resource_inventory/models.py index 5d87430..a143cfd 100644 --- a/src/resource_inventory/models.py +++ b/src/resource_inventory/models.py @@ -6,6 +6,14 @@ # are made available under the terms of the Apache License, Version 2.0 # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 +############################################################################################################################################################ +# Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others. +# Copyright (c) 2020 Sawyer Bergeron, Sean Smith, others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## from django.contrib.auth.models import User @@ -22,684 +30,9 @@ from collections import Counter from account.models import Lab from dashboard.utils import AbstractModelQuery -""" -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) - name = models.CharField(max_length=200, unique=True) - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64") - ]) - description = models.TextField() - labs = models.ManyToManyField(Lab, related_name="resourceprofiles") - - def validate(self): - validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$") - if not validname.match(self.name): - return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces." - else: - return None - - 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(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile') - nic_type = models.CharField( - max_length=50, - choices=[ - ("onboard", "onboard"), - ("pcie", "pcie") - ], - default="onboard" - ) - order = models.IntegerField(default=-1) - - def __str__(self): - return self.name + " for " + str(self.host) - - -class DiskProfile(models.Model): - id = models.AutoField(primary_key=True) - size = models.IntegerField() - media_type = models.CharField(max_length=50, choices=[ - ("SSD", "SSD"), - ("HDD", "HDD") - ]) - name = models.CharField(max_length=50) - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile') - rotation = models.IntegerField(default=0) - interface = models.CharField( - max_length=50, - choices=[ - ("sata", "sata"), - ("sas", "sas"), - ("ssd", "ssd"), - ("nvme", "nvme"), - ("scsi", "scsi"), - ("iscsi", "iscsi"), - ], - default="sata" - ) - - def __str__(self): - return self.name + " for " + str(self.host) - - -class CpuProfile(models.Model): - id = models.AutoField(primary_key=True) - cores = models.IntegerField() - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64") - ]) - cpus = models.IntegerField() - host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile') - cflags = models.TextField(null=True, blank=True) - - def __str__(self): - return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host) - - -class RamProfile(models.Model): - id = models.AutoField(primary_key=True) - amount = models.IntegerField() - channels = models.IntegerField() - 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 CloudInitFile(models.Model): - text = models.TextField() - - # higher priority is applied later, so "on top" of existing files - priority = models.IntegerField() - generated = models.BooleanField(default=False) - - @classmethod - def merge_strategy(cls): - return [ - {'name': 'list', 'settings': ['append']}, - {'name': 'dict', 'settings': ['recurse_list', 'replace']}, - ] - - @classmethod - def create(cls, text="", priority=0): - return CloudInitFile.objects.create(priority=priority, text=text) - - -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) - 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) - temporary = models.BooleanField(default=False) - copy_of = models.ForeignKey("ResourceTemplate", blank=True, null=True, on_delete=models.SET_NULL) - - # if these fields are empty ("") then they are implicitly "every vlan", - # otherwise we filter any allocations we try to instantiate against this list - # they should be represented as a json list of integers - private_vlan_pool = models.TextField(default="") - public_vlan_pool = models.TextField(default="") - - def private_vlan_pool_set(self): - if self.private_vlan_pool != "": - return set(json.loads(self.private_vlan_pool)) - else: - return None - - def public_vlan_pool_set(self): - if self.private_vlan_pool != "": - return set(json.loads(self.public_vlan_pool)) - else: - return None - - def getConfigs(self): - configs = self.resourceConfigurations.all() - return list(configs) - - def get_required_resources(self): - profiles = Counter([str(config.profile) for config in self.getConfigs()]) - return dict(profiles) - - 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 - - def release(self): - for pn in PhysicalNetwork.objects.filter(bundle=self).all(): - try: - pn.release() - except Exception as e: - print("Exception occurred while trying to release resource ", pn.vlan_id) - print(e) - traceback.print_exc() - - for resource in self.get_resources(): - try: - resource.release() - except Exception as e: - print("Exception occurred while trying to release resource ", resource) - print(e) - traceback.print_exc() - - def get_template_name(self): - if not self.template: - return "" - if not self.template.temporary: - return self.template.name - return self.template.copy_of.name - - -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 = models.CharField(max_length=3000, default="opnfv_host") - - cloud_init_files = models.ManyToManyField(CloudInitFile, blank=True) - - def __str__(self): - return str(self.name) - - def ci_file_list(self): - return list(self.cloud_init_files.order_by("priority").all()) - - +# Keep for now until migrations are made, otherwise django will get angry 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, blank=True, null=True) - profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE) - config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, blank=True, 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. - - Returns the desired configuration for this host as a - JSON object as defined in the rest api spec. - state is a ConfigState - """ - raise NotImplementedError("Must implement in concrete Resource classes") - - def reserve(self): - """Reserve this resource for its currently assigned booking.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def release(self): - """Make this resource available again for new boookings.""" - raise NotImplementedError("Must implement in concrete Resource classes") - - def get_interfaces(self): - """ - 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) - - -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" - image = self.config.image.lab_id if self.config else "unknown" - - return { - "id": self.labid, - "image": image, - "hostname": self.config.name, - "power": power, - "ipmi_create": str(ipmi) - } - - def get_interfaces(self): - return list(self.interfaces.all().order_by('bus_address')) - - def release(self): - self.bundle = None - 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 - - -def is_serializable(data): - try: - json.dumps(data) - return True - except Exception: - return False - - -class Opsys(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=100) - lab_id = models.CharField(max_length=100) - obsolete = models.BooleanField(default=False) - available = models.BooleanField(default=True) # marked true by Cobbler if it exists there - from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - - indexes = [ - models.Index(fields=['cobbler_id']) - ] - - def new_from_data(data): - opsys = Opsys() - opsys.update(data) - return opsys - - def serialize(self): - d = {} - for field in vars(self): - attr = getattr(self, field) - if is_serializable(attr): - d[field] = attr - return d - - def update(self, data): - for field in vars(self): - if field in data: - setattr(self, field, data[field] if data[field] else getattr(self, field)) - - def __str__(self): - return self.name - - -class Image(models.Model): - """Model for representing OS images / snapshots of hosts.""" - - id = models.AutoField(primary_key=True) - from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE) - architecture = models.CharField(max_length=50, choices=[ - ("x86_64", "x86_64"), - ("aarch64", "aarch64"), - ("unknown", "unknown"), - ]) - lab_id = models.CharField(max_length=100) - name = models.CharField(max_length=100) - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) - public = models.BooleanField(default=True) - description = models.TextField() - os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE) - - available = models.BooleanField(default=True) # marked True by cobbler if it exists there - obsolete = models.BooleanField(default=False) - - indexes = [ - models.Index(fields=['architecture']), - models.Index(fields=['cobbler_id']) - ] - - def __str__(self): - return self.name - - def is_obsolete(self): - return self.obsolete or self.os.obsolete - - def serialize(self): - d = {} - for field in vars(self): - attr = getattr(self, field) - if is_serializable(attr): - d[field] = attr - return d - - def update(self, data): - for field in vars(self): - if field in data: - setattr(self, field, data[field] if data[field] else getattr(self, field)) - - def new_from_data(data): - img = Image() - img.update(data) - return img - - 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=200) - bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks") - is_public = models.BooleanField() - - def __str__(self): - return self.name - - -class PhysicalNetwork(models.Model): - vlan_id = models.IntegerField() - generic_network = models.ForeignKey(Network, on_delete=models.CASCADE) - bundle = models.ForeignKey(ResourceBundle, null=True, blank=True, on_delete=models.CASCADE) - - def get_configuration(self, state): - """ - Get the network configuration. - - Collects info about each attached network interface and vlan, etc - """ - return {} - - def reserve(self): - """Reserve vlan(s) associated with this network.""" - return False - - def release(self): - from booking.models import Booking - - booking = Booking.objects.get(resource=self.bundle) - lab = booking.lab - vlan_manager = lab.vlan_manager - - if self.generic_network.is_public: - vlan_manager.release_public_vlan(self.vlan_id) - else: - vlan_manager.release_vlans([self.vlan_id]) - return False - - def __str__(self): - return 'Physical Network for ' + self.generic_network.name - - -class NetworkConnection(models.Model): - network = models.ForeignKey(Network, on_delete=models.CASCADE) - vlan_is_tagged = models.BooleanField() - - def __str__(self): - return 'Connection to ' + self.network.name - - -class Vlan(models.Model): - id = models.AutoField(primary_key=True) - vlan_id = models.IntegerField() - tagged = models.BooleanField() - public = models.BooleanField(default=False) - network = models.ForeignKey(PhysicalNetwork, on_delete=models.DO_NOTHING, null=True) - - def __str__(self): - return str(self.vlan_id) + ("_T" if self.tagged else "") - - -class InterfaceConfiguration(models.Model): - id = models.AutoField(primary_key=True) - profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) - resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs') - connections = models.ManyToManyField(NetworkConnection, blank=True) - - def __str__(self): - return "type " + str(self.profile) + " on host " + str(self.resource_config) - - -""" -OPNFV / Software configuration models -""" - - -class Scenario(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=300) - - def __str__(self): - return self.name - - -class Installer(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200) - sup_scenarios = models.ManyToManyField(Scenario, 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) - - -def create_resource_ref_string(for_hosts: [str]) -> str: - # need to sort the list, then do dump - for_hosts.sort() - - return json.dumps(for_hosts) - - -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) - 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="") - - def __str__(self): - return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario) - - -class OPNFVRole(models.Model): - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=200) - description = models.TextField() - - def __str__(self): - return self.name - + pass def get_sentinal_opnfv_role(): - return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.") - - -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): - id = models.AutoField(primary_key=True) - mac_address = models.CharField(max_length=17) - bus_address = models.CharField(max_length=50) - config = models.ManyToManyField(Vlan) - acts_as = models.OneToOneField(InterfaceConfiguration, blank=True, null=True, on_delete=models.CASCADE) - profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE) - - def __str__(self): - return self.mac_address + " on host " + str(self.profile.host.name) - - def clean(self, *args, **kwargs): - if self.acts_as and self.acts_as.profile != self.profile: - raise ValidationError("Interface Configuration's Interface Profile does not match Interface Profile chosen for Interface.") - super().clean(*args, **kwargs) - - def save(self, *args, **kwargs): - self.full_clean() - super().save(*args, **kwargs) - - -""" -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] + pass |