1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
3 # Copyright (c) 2020 Sawyer Bergeron, Sean Smith, others.
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
11 from django.contrib.auth.models import User
13 from django.core.exceptions import ValidationError
14 from django.db import models
15 from django.db.models import Q
20 from collections import Counter
22 from account.models import Lab
23 from dashboard.utils import AbstractModelQuery
26 Profiles of resources hosted by labs.
28 These describe hardware attributes of the different Resources a lab hosts.
29 A single Resource subclass (e.g. Server) may have instances that point to different
30 Profile models (e.g. an x86 server profile and armv8 server profile.
34 class ResourceProfile(models.Model):
35 id = models.AutoField(primary_key=True)
36 name = models.CharField(max_length=200, unique=True)
37 architecture = models.CharField(max_length=50, choices=[
39 ("aarch64", "aarch64")
41 description = models.TextField()
42 labs = models.ManyToManyField(Lab, related_name="resourceprofiles")
45 validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$")
46 if not validname.match(self.name):
47 return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces."
54 def get_resources(self, lab=None, working=True, unreserved=False):
56 Return a list of Resource objects which have this profile.
58 If lab is provided, only resources at that lab will be returned.
59 If working=True, will only return working hosts
62 query = Q(profile=self)
64 query = query & Q(lab=lab)
66 query = query & Q(working=True)
68 resources = ResourceQuery.filter(query)
71 resources = [r for r in resources if not r.is_reserved()]
76 class InterfaceProfile(models.Model):
77 id = models.AutoField(primary_key=True)
78 speed = models.IntegerField()
79 name = models.CharField(max_length=100)
80 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
81 nic_type = models.CharField(
84 ("onboard", "onboard"),
89 order = models.IntegerField(default=-1)
92 return self.name + " for " + str(self.host)
95 class DiskProfile(models.Model):
96 id = models.AutoField(primary_key=True)
97 size = models.IntegerField()
98 media_type = models.CharField(max_length=50, choices=[
102 name = models.CharField(max_length=50)
103 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile')
104 rotation = models.IntegerField(default=0)
105 interface = models.CharField(
119 return self.name + " for " + str(self.host)
122 class CpuProfile(models.Model):
123 id = models.AutoField(primary_key=True)
124 cores = models.IntegerField()
125 architecture = models.CharField(max_length=50, choices=[
126 ("x86_64", "x86_64"),
127 ("aarch64", "aarch64")
129 cpus = models.IntegerField()
130 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile')
131 cflags = models.TextField(null=True, blank=True)
134 return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host)
137 class RamProfile(models.Model):
138 id = models.AutoField(primary_key=True)
139 amount = models.IntegerField()
140 channels = models.IntegerField()
141 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='ramprofile')
144 return str(self.amount) + "G for " + str(self.host)
150 These models represent actual hardware resources
151 with varying degrees of abstraction.
155 class CloudInitFile(models.Model):
156 text = models.TextField()
158 # higher priority is applied later, so "on top" of existing files
159 priority = models.IntegerField()
160 generated = models.BooleanField(default=False)
163 def merge_strategy(cls):
165 {'name': 'list', 'settings': ['append']},
166 {'name': 'dict', 'settings': ['recurse_list', 'replace']},
170 def create(cls, text="", priority=0):
171 return CloudInitFile.objects.create(priority=priority, text=text)
174 class ResourceTemplate(models.Model):
176 Models a "template" of a complete, configured collection of resources that can be booked.
178 For example, this may represent a Pharos POD. This model is a template of the actual
179 resources that will be booked. This model can be "instantiated" into real resource models
180 across multiple different bookings.
183 # TODO: template might not be a good name because this is a collection of lots of configured resources
184 id = models.AutoField(primary_key=True)
185 name = models.CharField(max_length=300)
186 xml = models.TextField()
187 owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
188 lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL, related_name="resourcetemplates")
189 description = models.CharField(max_length=1000, default="")
190 public = models.BooleanField(default=False)
191 temporary = models.BooleanField(default=False)
192 copy_of = models.ForeignKey("ResourceTemplate", blank=True, null=True, on_delete=models.SET_NULL)
194 # if these fields are empty ("") then they are implicitly "every vlan",
195 # otherwise we filter any allocations we try to instantiate against this list
196 # they should be represented as a json list of integers
197 private_vlan_pool = models.TextField(default="")
198 public_vlan_pool = models.TextField(default="")
200 def private_vlan_pool_set(self):
201 if self.private_vlan_pool != "":
202 return set(json.loads(self.private_vlan_pool))
206 def public_vlan_pool_set(self):
207 if self.private_vlan_pool != "":
208 return set(json.loads(self.public_vlan_pool))
212 def getConfigs(self):
213 configs = self.resourceConfigurations.all()
216 def get_required_resources(self):
217 profiles = Counter([str(config.profile) for config in self.getConfigs()])
218 return dict(profiles)
224 class ResourceBundle(models.Model):
226 Collection of Resource objects.
228 This is just a way of aggregating all the resources in a booking into a single model.
231 template = models.ForeignKey(ResourceTemplate, on_delete=models.SET_NULL, null=True)
234 if self.template is None:
235 return "Resource bundle " + str(self.id) + " with no template"
236 return "instance of " + str(self.template)
238 def get_resources(self):
239 return ResourceQuery.filter(bundle=self)
241 def get_resource_with_role(self, role):
246 for pn in PhysicalNetwork.objects.filter(bundle=self).all():
249 except Exception as e:
250 print("Exception occurred while trying to release resource ", pn.vlan_id)
252 traceback.print_exc()
254 for resource in self.get_resources():
257 except Exception as e:
258 print("Exception occurred while trying to release resource ", resource)
260 traceback.print_exc()
262 def get_template_name(self):
263 if not self.template:
265 if not self.template.temporary:
266 return self.template.name
267 return self.template.copy_of.name
270 class ResourceConfiguration(models.Model):
271 """Model to represent a complete configuration for a single physical Resource."""
273 id = models.AutoField(primary_key=True)
274 profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
275 image = models.ForeignKey("Image", on_delete=models.PROTECT)
276 template = models.ForeignKey(ResourceTemplate, related_name="resourceConfigurations", null=True, on_delete=models.CASCADE)
277 is_head_node = models.BooleanField(default=False)
278 name = models.CharField(max_length=3000, default="opnfv_host")
280 cloud_init_files = models.ManyToManyField(CloudInitFile, blank=True)
283 return str(self.name)
285 def ci_file_list(self):
286 return list(self.cloud_init_files.order_by("priority").all())
289 def get_default_remote_info():
290 return RemoteInfo.objects.get_or_create(
292 mac_address="default",
295 management_type="default",
300 class Resource(models.Model):
302 Super class for all hardware resource models.
304 Defines methods that must be implemented and common database fields.
305 Any new kind of Resource a lab wants to host (White box switch, traffic generator, etc)
306 should inherit from this class and fulfill the functional interface
312 bundle = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, blank=True, null=True)
313 profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
314 config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, blank=True, null=True)
315 working = models.BooleanField(default=True)
316 vendor = models.CharField(max_length=100, default="unknown")
317 model = models.CharField(max_length=150, default="unknown")
318 interfaces = models.ManyToManyField("Interface")
319 remote_management = models.ForeignKey("RemoteInfo", default=get_default_remote_info, on_delete=models.SET(get_default_remote_info))
320 labid = models.CharField(max_length=200, default="default_id", unique=True)
321 lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
323 def get_configuration(self, state):
325 Get configuration of Resource.
327 Returns the desired configuration for this host as a
328 JSON object as defined in the rest api spec.
329 state is a ConfigState
331 raise NotImplementedError("Must implement in concrete Resource classes")
334 """Reserve this resource for its currently assigned booking."""
335 raise NotImplementedError("Must implement in concrete Resource classes")
338 """Make this resource available again for new boookings."""
339 raise NotImplementedError("Must implement in concrete Resource classes")
341 def get_interfaces(self):
343 Return a list of interfaces on this resource.
345 The ordering of interfaces should be consistent.
347 raise NotImplementedError("Must implement in concrete Resource classes")
349 def is_reserved(self):
350 """Return True if this Resource is reserved."""
351 raise NotImplementedError("Must implement in concrete Resource classes")
353 def same_instance(self, other):
354 """Return True if this Resource is the same instance as other."""
355 raise NotImplementedError("Must implement in concrete Resource classes")
357 def save(self, *args, **kwargs):
358 """Assert that labid is unique across all Resource models."""
359 res = ResourceQuery.filter(labid=self.labid)
361 raise ValidationError("Too many resources with labid " + str(self.labid))
364 if not self.same_instance(res[0]):
365 raise ValidationError("Too many resources with labid " + str(self.labid))
366 super().save(*args, **kwargs)
369 class RemoteInfo(models.Model):
370 address = models.CharField(max_length=15)
371 mac_address = models.CharField(max_length=17)
372 password = models.CharField(max_length=100)
373 user = models.CharField(max_length=100)
374 management_type = models.CharField(max_length=50, default="ipmi")
375 versions = models.CharField(max_length=100) # json serialized list of floats
378 class Server(Resource):
379 """Resource subclass - a basic baremetal server."""
381 booked = models.BooleanField(default=False)
382 name = models.CharField(max_length=200, unique=True)
387 def get_configuration(self, state):
388 ipmi = state == ConfigState.NEW
389 power = "off" if state == ConfigState.CLEAN else "on"
390 image = self.config.image.lab_id if self.config else "unknown"
395 "hostname": self.config.name,
397 "ipmi_create": str(ipmi)
400 def get_interfaces(self):
401 return list(self.interfaces.all().order_by('bus_address'))
412 def is_reserved(self):
415 def same_instance(self, other):
416 return isinstance(other, Server) and other.name == self.name
419 def is_serializable(data):
427 class Opsys(models.Model):
428 id = models.AutoField(primary_key=True)
429 name = models.CharField(max_length=100)
430 lab_id = models.CharField(max_length=100)
431 obsolete = models.BooleanField(default=False)
432 available = models.BooleanField(default=True) # marked true by Cobbler if it exists there
433 from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
436 models.Index(fields=['cobbler_id'])
439 def new_from_data(data):
446 for field in vars(self):
447 attr = getattr(self, field)
448 if is_serializable(attr):
452 def update(self, data):
453 for field in vars(self):
455 setattr(self, field, data[field] if data[field] else getattr(self, field))
461 class Image(models.Model):
462 """Model for representing OS images / snapshots of hosts."""
464 id = models.AutoField(primary_key=True)
465 from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
466 architecture = models.CharField(max_length=50, choices=[
467 ("x86_64", "x86_64"),
468 ("aarch64", "aarch64"),
469 ("unknown", "unknown"),
471 lab_id = models.CharField(max_length=100)
472 name = models.CharField(max_length=100)
473 owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
474 public = models.BooleanField(default=True)
475 description = models.TextField()
476 os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
478 available = models.BooleanField(default=True) # marked True by cobbler if it exists there
479 obsolete = models.BooleanField(default=False)
482 models.Index(fields=['architecture']),
483 models.Index(fields=['cobbler_id'])
489 def is_obsolete(self):
490 return self.obsolete or self.os.obsolete
494 for field in vars(self):
495 attr = getattr(self, field)
496 if is_serializable(attr):
500 def update(self, data):
501 for field in vars(self):
503 setattr(self, field, data[field] if data[field] else getattr(self, field))
505 def new_from_data(data):
511 for resource in ResourceQuery.filter(config__image=self):
512 if resource.is_reserved():
519 Networking configuration models
523 class Network(models.Model):
524 id = models.AutoField(primary_key=True)
525 name = models.CharField(max_length=200)
526 bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks")
527 is_public = models.BooleanField()
533 class PhysicalNetwork(models.Model):
534 vlan_id = models.IntegerField()
535 generic_network = models.ForeignKey(Network, on_delete=models.CASCADE)
536 bundle = models.ForeignKey(ResourceBundle, null=True, blank=True, on_delete=models.CASCADE)
538 def get_configuration(self, state):
540 Get the network configuration.
542 Collects info about each attached network interface and vlan, etc
547 """Reserve vlan(s) associated with this network."""
551 from booking.models import Booking
553 booking = Booking.objects.get(resource=self.bundle)
555 vlan_manager = lab.vlan_manager
557 if self.generic_network.is_public:
558 vlan_manager.release_public_vlan(self.vlan_id)
560 vlan_manager.release_vlans([self.vlan_id])
564 return 'Physical Network for ' + self.generic_network.name
567 class NetworkConnection(models.Model):
568 network = models.ForeignKey(Network, on_delete=models.CASCADE)
569 vlan_is_tagged = models.BooleanField()
572 return 'Connection to ' + self.network.name
575 class Vlan(models.Model):
576 id = models.AutoField(primary_key=True)
577 vlan_id = models.IntegerField()
578 tagged = models.BooleanField()
579 public = models.BooleanField(default=False)
580 network = models.ForeignKey(PhysicalNetwork, on_delete=models.DO_NOTHING, null=True)
583 return str(self.vlan_id) + ("_T" if self.tagged else "")
586 class InterfaceConfiguration(models.Model):
587 id = models.AutoField(primary_key=True)
588 profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
589 resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs')
590 connections = models.ManyToManyField(NetworkConnection, blank=True)
593 return "type " + str(self.profile) + " on host " + str(self.resource_config)
597 OPNFV / Software configuration models
601 class Scenario(models.Model):
602 id = models.AutoField(primary_key=True)
603 name = models.CharField(max_length=300)
609 class Installer(models.Model):
610 id = models.AutoField(primary_key=True)
611 name = models.CharField(max_length=200)
612 sup_scenarios = models.ManyToManyField(Scenario, blank=True)
618 class NetworkRole(models.Model):
619 name = models.CharField(max_length=100)
620 network = models.ForeignKey(Network, on_delete=models.CASCADE)
623 def create_resource_ref_string(for_hosts: [str]) -> str:
624 # need to sort the list, then do dump
627 return json.dumps(for_hosts)
630 class OPNFVConfig(models.Model):
631 id = models.AutoField(primary_key=True)
632 installer = models.ForeignKey(Installer, on_delete=models.CASCADE)
633 scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE)
634 template = models.ForeignKey(ResourceTemplate, related_name="opnfv_config", on_delete=models.CASCADE)
635 networks = models.ManyToManyField(NetworkRole)
636 name = models.CharField(max_length=300, blank=True, default="")
637 description = models.CharField(max_length=600, blank=True, default="")
640 return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario)
643 class OPNFVRole(models.Model):
644 id = models.AutoField(primary_key=True)
645 name = models.CharField(max_length=200)
646 description = models.TextField()
652 def get_sentinal_opnfv_role():
653 return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
656 class ResourceOPNFVConfig(models.Model):
657 role = models.ForeignKey(OPNFVRole, related_name="resource_opnfv_configs", on_delete=models.CASCADE)
658 resource_config = models.ForeignKey(ResourceConfiguration, related_name="resource_opnfv_config", on_delete=models.CASCADE)
659 opnfv_config = models.ForeignKey(OPNFVConfig, related_name="resource_opnfv_config", on_delete=models.CASCADE)
662 class Interface(models.Model):
663 id = models.AutoField(primary_key=True)
664 mac_address = models.CharField(max_length=17)
665 bus_address = models.CharField(max_length=50)
666 config = models.ManyToManyField(Vlan)
667 acts_as = models.OneToOneField(InterfaceConfiguration, blank=True, null=True, on_delete=models.CASCADE)
668 profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
671 return self.mac_address + " on host " + str(self.profile.host.name)
673 def clean(self, *args, **kwargs):
674 if self.acts_as and self.acts_as.profile != self.profile:
675 raise ValidationError("Interface Configuration's Interface Profile does not match Interface Profile chosen for Interface.")
676 super().clean(*args, **kwargs)
678 def save(self, *args, **kwargs):
680 super().save(*args, **kwargs)
684 Some Enums for dealing with global constants.
688 class OPNFV_SETTINGS():
689 """This is a static configuration class."""
691 # all the required network types in PDF/IDF spec
692 NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
701 RESOURCE_TYPES = [Server]
704 class ResourceQuery(AbstractModelQuery):
705 model_list = [Server]