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
12 from django.core.exceptions import ValidationError
13 from django.db import models
14 from django.db.models import Q
18 from collections import Counter
20 from account.models import Lab
21 from dashboard.utils import AbstractModelQuery
25 Profiles of resources hosted by labs.
27 These describe hardware attributes of the different Resources a lab hosts.
28 A single Resource subclass (e.g. Server) may have instances that point to different
29 Profile models (e.g. an x86 server profile and armv8 server profile.
33 class ResourceProfile(models.Model):
34 id = models.AutoField(primary_key=True)
35 name = models.CharField(max_length=200, unique=True)
36 description = models.TextField()
37 labs = models.ManyToManyField(Lab, related_name="resourceprofiles")
40 validname = re.compile(r"^[A-Za-z0-9\-\_\.\/\, ]+$")
41 if not validname.match(self.name):
42 return "Invalid host profile name given. Name must only use A-Z, a-z, 0-9, hyphens, underscores, dots, commas, or spaces."
49 def get_resources(self, lab=None, working=True, unreserved=False):
51 Return a list of Resource objects which have this profile.
53 If lab is provided, only resources at that lab will be returned.
54 If working=True, will only return working hosts
57 query = Q(profile=self)
59 query = query & Q(lab=lab)
61 query = query & Q(working=True)
63 resources = ResourceQuery.filter(query)
66 resources = [r for r in resources if not r.is_reserved()]
71 class InterfaceProfile(models.Model):
72 id = models.AutoField(primary_key=True)
73 speed = models.IntegerField()
74 name = models.CharField(max_length=100)
75 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='interfaceprofile')
76 nic_type = models.CharField(
79 ("onboard", "onboard"),
84 order = models.IntegerField(default=-1)
87 return self.name + " for " + str(self.host)
90 class DiskProfile(models.Model):
91 id = models.AutoField(primary_key=True)
92 size = models.IntegerField()
93 media_type = models.CharField(max_length=50, choices=[
97 name = models.CharField(max_length=50)
98 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='storageprofile')
99 rotation = models.IntegerField(default=0)
100 interface = models.CharField(
114 return self.name + " for " + str(self.host)
117 class CpuProfile(models.Model):
118 id = models.AutoField(primary_key=True)
119 cores = models.IntegerField()
120 architecture = models.CharField(max_length=50, choices=[
121 ("x86_64", "x86_64"),
122 ("aarch64", "aarch64")
124 cpus = models.IntegerField()
125 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile')
126 cflags = models.TextField(null=True, blank=True)
129 return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host)
132 class RamProfile(models.Model):
133 id = models.AutoField(primary_key=True)
134 amount = models.IntegerField()
135 channels = models.IntegerField()
136 host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='ramprofile')
139 return str(self.amount) + "G for " + str(self.host)
145 These models represent actual hardware resources
146 with varying degrees of abstraction.
150 class ResourceTemplate(models.Model):
152 Models a "template" of a complete, configured collection of resources that can be booked.
154 For example, this may represent a Pharos POD. This model is a template of the actual
155 resources that will be booked. This model can be "instantiated" into real resource models
156 across multiple different bookings.
159 # TODO: template might not be a good name because this is a collection of lots of configured resources
160 id = models.AutoField(primary_key=True)
161 name = models.CharField(max_length=300)
162 xml = models.TextField()
163 owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
164 lab = models.ForeignKey(Lab, null=True, on_delete=models.SET_NULL, related_name="resourcetemplates")
165 description = models.CharField(max_length=1000, default="")
166 public = models.BooleanField(default=False)
167 temporary = models.BooleanField(default=False)
168 copy_of = models.ForeignKey("ResourceTemplate", blank=True, null=True, on_delete=models.SET_NULL)
170 def getConfigs(self):
171 configs = self.resourceConfigurations.all()
174 def get_required_resources(self):
175 profiles = Counter([str(config.profile) for config in self.getConfigs()])
176 return dict(profiles)
182 class ResourceBundle(models.Model):
184 Collection of Resource objects.
186 This is just a way of aggregating all the resources in a booking into a single model.
189 template = models.ForeignKey(ResourceTemplate, on_delete=models.SET_NULL, null=True)
192 if self.template is None:
193 return "Resource bundle " + str(self.id) + " with no template"
194 return "instance of " + str(self.template)
196 def get_resources(self):
197 return ResourceQuery.filter(bundle=self)
199 def get_resource_with_role(self, role):
204 for pn in PhysicalNetwork.objects.filter(bundle=self).all():
207 except Exception as e:
208 print("Exception occurred while trying to release resource ", pn.vlan_id)
210 traceback.print_exc()
212 for resource in self.get_resources():
215 except Exception as e:
216 print("Exception occurred while trying to release resource ", resource)
218 traceback.print_exc()
220 def get_template_name(self):
221 if not self.template:
223 if not self.template.temporary:
224 return self.template.name
225 return self.template.copy_of.name
228 class ResourceConfiguration(models.Model):
229 """Model to represent a complete configuration for a single physical Resource."""
231 id = models.AutoField(primary_key=True)
232 profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
233 image = models.ForeignKey("Image", on_delete=models.PROTECT)
234 template = models.ForeignKey(ResourceTemplate, related_name="resourceConfigurations", null=True, on_delete=models.CASCADE)
235 is_head_node = models.BooleanField(default=False)
236 name = models.CharField(max_length=3000, default="opnfv_host")
239 return str(self.name)
242 def get_default_remote_info():
243 return RemoteInfo.objects.get_or_create(
245 mac_address="default",
248 management_type="default",
253 class Resource(models.Model):
255 Super class for all hardware resource models.
257 Defines methods that must be implemented and common database fields.
258 Any new kind of Resource a lab wants to host (White box switch, traffic generator, etc)
259 should inherit from this class and fulfill the functional interface
265 bundle = models.ForeignKey(ResourceBundle, on_delete=models.SET_NULL, blank=True, null=True)
266 profile = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
267 config = models.ForeignKey(ResourceConfiguration, on_delete=models.SET_NULL, blank=True, null=True)
268 working = models.BooleanField(default=True)
269 vendor = models.CharField(max_length=100, default="unknown")
270 model = models.CharField(max_length=150, default="unknown")
271 interfaces = models.ManyToManyField("Interface")
272 remote_management = models.ForeignKey("RemoteInfo", default=get_default_remote_info, on_delete=models.SET(get_default_remote_info))
273 labid = models.CharField(max_length=200, default="default_id", unique=True)
274 lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
276 def get_configuration(self, state):
278 Get configuration of Resource.
280 Returns the desired configuration for this host as a
281 JSON object as defined in the rest api spec.
282 state is a ConfigState
284 raise NotImplementedError("Must implement in concrete Resource classes")
287 """Reserve this resource for its currently assigned booking."""
288 raise NotImplementedError("Must implement in concrete Resource classes")
291 """Make this resource available again for new boookings."""
292 raise NotImplementedError("Must implement in concrete Resource classes")
294 def get_interfaces(self):
296 Return a list of interfaces on this resource.
298 The ordering of interfaces should be consistent.
300 raise NotImplementedError("Must implement in concrete Resource classes")
302 def is_reserved(self):
303 """Return True if this Resource is reserved."""
304 raise NotImplementedError("Must implement in concrete Resource classes")
306 def same_instance(self, other):
307 """Return True if this Resource is the same instance as other."""
308 raise NotImplementedError("Must implement in concrete Resource classes")
310 def save(self, *args, **kwargs):
311 """Assert that labid is unique across all Resource models."""
312 res = ResourceQuery.filter(labid=self.labid)
314 raise ValidationError("Too many resources with labid " + str(self.labid))
317 if not self.same_instance(res[0]):
318 raise ValidationError("Too many resources with labid " + str(self.labid))
319 super().save(*args, **kwargs)
322 class RemoteInfo(models.Model):
323 address = models.CharField(max_length=15)
324 mac_address = models.CharField(max_length=17)
325 password = models.CharField(max_length=100)
326 user = models.CharField(max_length=100)
327 management_type = models.CharField(max_length=50, default="ipmi")
328 versions = models.CharField(max_length=100) # json serialized list of floats
331 class Server(Resource):
332 """Resource subclass - a basic baremetal server."""
334 booked = models.BooleanField(default=False)
335 name = models.CharField(max_length=200, unique=True)
340 def get_configuration(self, state):
341 ipmi = state == ConfigState.NEW
342 power = "off" if state == ConfigState.CLEAN else "on"
343 image = self.config.image.lab_id if self.config else "unknown"
348 "hostname": self.config.name,
350 "ipmi_create": str(ipmi)
353 def get_interfaces(self):
354 return list(self.interfaces.all().order_by('bus_address'))
365 def is_reserved(self):
368 def same_instance(self, other):
369 return isinstance(other, Server) and other.name == self.name
372 class Opsys(models.Model):
373 id = models.AutoField(primary_key=True)
374 name = models.CharField(max_length=100)
375 sup_installers = models.ManyToManyField("Installer", blank=True)
381 class Image(models.Model):
382 """Model for representing OS images / snapshots of hosts."""
384 id = models.AutoField(primary_key=True)
385 lab_id = models.IntegerField() # ID the lab who holds this image knows
386 from_lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
387 name = models.CharField(max_length=200)
388 owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
389 public = models.BooleanField(default=True)
390 host_type = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE)
391 description = models.TextField()
392 os = models.ForeignKey(Opsys, null=True, on_delete=models.CASCADE)
398 for resource in ResourceQuery.filter(config__image=self):
399 if resource.is_reserved():
406 Networking configuration models
410 class Network(models.Model):
411 id = models.AutoField(primary_key=True)
412 name = models.CharField(max_length=100)
413 bundle = models.ForeignKey(ResourceTemplate, on_delete=models.CASCADE, related_name="networks")
414 is_public = models.BooleanField()
420 class PhysicalNetwork(models.Model):
421 vlan_id = models.IntegerField()
422 generic_network = models.ForeignKey(Network, on_delete=models.CASCADE)
423 bundle = models.ForeignKey(ResourceBundle, null=True, blank=True, on_delete=models.CASCADE)
425 def get_configuration(self, state):
427 Get the network configuration.
429 Collects info about each attached network interface and vlan, etc
434 """Reserve vlan(s) associated with this network."""
438 from booking.models import Booking
440 booking = Booking.objects.get(resource=self.bundle)
442 vlan_manager = lab.vlan_manager
444 if self.generic_network.is_public:
445 vlan_manager.release_public_vlan(self.vlan_id)
447 vlan_manager.release_vlans([self.vlan_id])
451 return 'Physical Network for ' + self.generic_network.name
454 class NetworkConnection(models.Model):
455 network = models.ForeignKey(Network, on_delete=models.CASCADE)
456 vlan_is_tagged = models.BooleanField()
459 return 'Connection to ' + self.network.name
462 class Vlan(models.Model):
463 id = models.AutoField(primary_key=True)
464 vlan_id = models.IntegerField()
465 tagged = models.BooleanField()
466 public = models.BooleanField(default=False)
467 network = models.ForeignKey(PhysicalNetwork, on_delete=models.DO_NOTHING, null=True)
470 return str(self.vlan_id) + ("_T" if self.tagged else "")
473 class InterfaceConfiguration(models.Model):
474 id = models.AutoField(primary_key=True)
475 profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
476 resource_config = models.ForeignKey(ResourceConfiguration, on_delete=models.CASCADE, related_name='interface_configs')
477 connections = models.ManyToManyField(NetworkConnection, blank=True)
480 return "type " + str(self.profile) + " on host " + str(self.resource_config)
484 OPNFV / Software configuration models
488 class Scenario(models.Model):
489 id = models.AutoField(primary_key=True)
490 name = models.CharField(max_length=300)
496 class Installer(models.Model):
497 id = models.AutoField(primary_key=True)
498 name = models.CharField(max_length=200)
499 sup_scenarios = models.ManyToManyField(Scenario, blank=True)
505 class NetworkRole(models.Model):
506 name = models.CharField(max_length=100)
507 network = models.ForeignKey(Network, on_delete=models.CASCADE)
510 class OPNFVConfig(models.Model):
511 id = models.AutoField(primary_key=True)
512 installer = models.ForeignKey(Installer, on_delete=models.CASCADE)
513 scenario = models.ForeignKey(Scenario, on_delete=models.CASCADE)
514 template = models.ForeignKey(ResourceTemplate, related_name="opnfv_config", on_delete=models.CASCADE)
515 networks = models.ManyToManyField(NetworkRole)
516 name = models.CharField(max_length=300, blank=True, default="")
517 description = models.CharField(max_length=600, blank=True, default="")
520 return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario)
523 class OPNFVRole(models.Model):
524 id = models.AutoField(primary_key=True)
525 name = models.CharField(max_length=200)
526 description = models.TextField()
532 def get_sentinal_opnfv_role():
533 return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
536 class ResourceOPNFVConfig(models.Model):
537 role = models.ForeignKey(OPNFVRole, related_name="resource_opnfv_configs", on_delete=models.CASCADE)
538 resource_config = models.ForeignKey(ResourceConfiguration, related_name="resource_opnfv_config", on_delete=models.CASCADE)
539 opnfv_config = models.ForeignKey(OPNFVConfig, related_name="resource_opnfv_config", on_delete=models.CASCADE)
542 class Interface(models.Model):
543 id = models.AutoField(primary_key=True)
544 mac_address = models.CharField(max_length=17)
545 bus_address = models.CharField(max_length=50)
546 config = models.ManyToManyField(Vlan)
547 acts_as = models.OneToOneField(InterfaceConfiguration, blank=True, null=True, on_delete=models.CASCADE)
548 profile = models.ForeignKey(InterfaceProfile, on_delete=models.CASCADE)
551 return self.mac_address + " on host " + str(self.profile.host.name)
553 def clean(self, *args, **kwargs):
554 if self.acts_as and self.acts_as.profile != self.profile:
555 raise ValidationError("Interface Configuration's Interface Profile does not match Interface Profile chosen for Interface.")
556 super().clean(*args, **kwargs)
558 def save(self, *args, **kwargs):
560 super().save(*args, **kwargs)
564 Some Enums for dealing with global constants.
568 class OPNFV_SETTINGS():
569 """This is a static configuration class."""
571 # all the required network types in PDF/IDF spec
572 NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
581 RESOURCE_TYPES = [Server]
584 class ResourceQuery(AbstractModelQuery):
585 model_list = [Server]