Fix hostname default to be valid linux hostname
[laas.git] / src / resource_inventory / models.py
1 ##############################################################################
2 # Copyright (c) 2018 Sawyer Bergeron, Parker Berberian, and others.
3 # Copyright (c) 2020 Sawyer Bergeron, Sean Smith, others.
4 #
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 ##############################################################################
10
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
15 import traceback
16
17 import re
18 from collections import Counter
19
20 from account.models import Lab
21 from dashboard.utils import AbstractModelQuery
22
23
24 """
25 Profiles of resources hosted by labs.
26
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.
30 """
31
32
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")
38
39     def validate(self):
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."
43         else:
44             return None
45
46     def __str__(self):
47         return self.name
48
49     def get_resources(self, lab=None, working=True, unreserved=False):
50         """
51         Return a list of Resource objects which have this profile.
52
53         If lab is provided, only resources at that lab will be returned.
54         If working=True, will only return working hosts
55         """
56         resources = []
57         query = Q(profile=self)
58         if lab:
59             query = query & Q(lab=lab)
60         if working:
61             query = query & Q(working=True)
62
63         resources = ResourceQuery.filter(query)
64
65         if unreserved:
66             resources = [r for r in resources if not r.is_reserved()]
67
68         return resources
69
70
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(
77         max_length=50,
78         choices=[
79             ("onboard", "onboard"),
80             ("pcie", "pcie")
81         ],
82         default="onboard"
83     )
84     order = models.IntegerField(default=-1)
85
86     def __str__(self):
87         return self.name + " for " + str(self.host)
88
89
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=[
94         ("SSD", "SSD"),
95         ("HDD", "HDD")
96     ])
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(
101         max_length=50,
102         choices=[
103             ("sata", "sata"),
104             ("sas", "sas"),
105             ("ssd", "ssd"),
106             ("nvme", "nvme"),
107             ("scsi", "scsi"),
108             ("iscsi", "iscsi"),
109         ],
110         default="sata"
111     )
112
113     def __str__(self):
114         return self.name + " for " + str(self.host)
115
116
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")
123     ])
124     cpus = models.IntegerField()
125     host = models.ForeignKey(ResourceProfile, on_delete=models.CASCADE, related_name='cpuprofile')
126     cflags = models.TextField(null=True, blank=True)
127
128     def __str__(self):
129         return str(self.architecture) + " " + str(self.cpus) + "S" + str(self.cores) + " C for " + str(self.host)
130
131
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')
137
138     def __str__(self):
139         return str(self.amount) + "G for " + str(self.host)
140
141
142 """
143 Resource Models
144
145 These models represent actual hardware resources
146 with varying degrees of abstraction.
147 """
148
149
150 class ResourceTemplate(models.Model):
151     """
152     Models a "template" of a complete, configured collection of resources that can be booked.
153
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.
157     """
158
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)
169
170     def getConfigs(self):
171         configs = self.resourceConfigurations.all()
172         return list(configs)
173
174     def get_required_resources(self):
175         profiles = Counter([str(config.profile) for config in self.getConfigs()])
176         return dict(profiles)
177
178     def __str__(self):
179         return self.name
180
181
182 class ResourceBundle(models.Model):
183     """
184     Collection of Resource objects.
185
186     This is just a way of aggregating all the resources in a booking into a single model.
187     """
188
189     template = models.ForeignKey(ResourceTemplate, on_delete=models.SET_NULL, null=True)
190
191     def __str__(self):
192         if self.template is None:
193             return "Resource bundle " + str(self.id) + " with no template"
194         return "instance of " + str(self.template)
195
196     def get_resources(self):
197         return ResourceQuery.filter(bundle=self)
198
199     def get_resource_with_role(self, role):
200         # TODO
201         pass
202
203     def release(self):
204         for pn in PhysicalNetwork.objects.filter(bundle=self).all():
205             try:
206                 pn.release()
207             except Exception as e:
208                 print("Exception occurred while trying to release resource ", pn.vlan_id)
209                 print(e)
210                 traceback.print_exc()
211
212         for resource in self.get_resources():
213             try:
214                 resource.release()
215             except Exception as e:
216                 print("Exception occurred while trying to release resource ", resource)
217                 print(e)
218                 traceback.print_exc()
219
220     def get_template_name(self):
221         if not self.template:
222             return ""
223         if not self.template.temporary:
224             return self.template.name
225         return self.template.copy_of.name
226
227
228 class ResourceConfiguration(models.Model):
229     """Model to represent a complete configuration for a single physical Resource."""
230
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")
237
238     def __str__(self):
239         return str(self.name)
240
241
242 def get_default_remote_info():
243     return RemoteInfo.objects.get_or_create(
244         address="default",
245         mac_address="default",
246         password="default",
247         user="default",
248         management_type="default",
249         versions="[default]"
250     )[0].pk
251
252
253 class Resource(models.Model):
254     """
255     Super class for all hardware resource models.
256
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
260     """
261
262     class Meta:
263         abstract = True
264
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)
275
276     def get_configuration(self, state):
277         """
278         Get configuration of Resource.
279
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
283         """
284         raise NotImplementedError("Must implement in concrete Resource classes")
285
286     def reserve(self):
287         """Reserve this resource for its currently assigned booking."""
288         raise NotImplementedError("Must implement in concrete Resource classes")
289
290     def release(self):
291         """Make this resource available again for new boookings."""
292         raise NotImplementedError("Must implement in concrete Resource classes")
293
294     def get_interfaces(self):
295         """
296         Return a list of interfaces on this resource.
297
298         The ordering of interfaces should be consistent.
299         """
300         raise NotImplementedError("Must implement in concrete Resource classes")
301
302     def is_reserved(self):
303         """Return True if this Resource is reserved."""
304         raise NotImplementedError("Must implement in concrete Resource classes")
305
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")
309
310     def save(self, *args, **kwargs):
311         """Assert that labid is unique across all Resource models."""
312         res = ResourceQuery.filter(labid=self.labid)
313         if len(res) > 1:
314             raise ValidationError("Too many resources with labid " + str(self.labid))
315
316         if len(res) == 1:
317             if not self.same_instance(res[0]):
318                 raise ValidationError("Too many resources with labid " + str(self.labid))
319         super().save(*args, **kwargs)
320
321
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
329
330
331 class Server(Resource):
332     """Resource subclass - a basic baremetal server."""
333
334     booked = models.BooleanField(default=False)
335     name = models.CharField(max_length=200, unique=True)
336
337     def __str__(self):
338         return self.name
339
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"
344
345         return {
346             "id": self.labid,
347             "image": image,
348             "hostname": self.config.name,
349             "power": power,
350             "ipmi_create": str(ipmi)
351         }
352
353     def get_interfaces(self):
354         return list(self.interfaces.all().order_by('bus_address'))
355
356     def release(self):
357         self.bundle = None
358         self.booked = False
359         self.save()
360
361     def reserve(self):
362         self.booked = True
363         self.save()
364
365     def is_reserved(self):
366         return self.booked
367
368     def same_instance(self, other):
369         return isinstance(other, Server) and other.name == self.name
370
371
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)
376
377     def __str__(self):
378         return self.name
379
380
381 class Image(models.Model):
382     """Model for representing OS images / snapshots of hosts."""
383
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)
393
394     def __str__(self):
395         return self.name
396
397     def in_use(self):
398         for resource in ResourceQuery.filter(config__image=self):
399             if resource.is_reserved():
400                 return True
401
402         return False
403
404
405 """
406 Networking configuration models
407 """
408
409
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()
415
416     def __str__(self):
417         return self.name
418
419
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)
424
425     def get_configuration(self, state):
426         """
427         Get the network configuration.
428
429         Collects info about each attached network interface and vlan, etc
430         """
431         return {}
432
433     def reserve(self):
434         """Reserve vlan(s) associated with this network."""
435         return False
436
437     def release(self):
438         from booking.models import Booking
439
440         booking = Booking.objects.get(resource=self.bundle)
441         lab = booking.lab
442         vlan_manager = lab.vlan_manager
443
444         if self.generic_network.is_public:
445             vlan_manager.release_public_vlan(self.vlan_id)
446         else:
447             vlan_manager.release_vlans([self.vlan_id])
448         return False
449
450     def __str__(self):
451         return 'Physical Network for ' + self.generic_network.name
452
453
454 class NetworkConnection(models.Model):
455     network = models.ForeignKey(Network, on_delete=models.CASCADE)
456     vlan_is_tagged = models.BooleanField()
457
458     def __str__(self):
459         return 'Connection to ' + self.network.name
460
461
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)
468
469     def __str__(self):
470         return str(self.vlan_id) + ("_T" if self.tagged else "")
471
472
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)
478
479     def __str__(self):
480         return "type " + str(self.profile) + " on host " + str(self.resource_config)
481
482
483 """
484 OPNFV / Software configuration models
485 """
486
487
488 class Scenario(models.Model):
489     id = models.AutoField(primary_key=True)
490     name = models.CharField(max_length=300)
491
492     def __str__(self):
493         return self.name
494
495
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)
500
501     def __str__(self):
502         return self.name
503
504
505 class NetworkRole(models.Model):
506     name = models.CharField(max_length=100)
507     network = models.ForeignKey(Network, on_delete=models.CASCADE)
508
509
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="")
518
519     def __str__(self):
520         return "OPNFV job with " + str(self.installer) + " and " + str(self.scenario)
521
522
523 class OPNFVRole(models.Model):
524     id = models.AutoField(primary_key=True)
525     name = models.CharField(max_length=200)
526     description = models.TextField()
527
528     def __str__(self):
529         return self.name
530
531
532 def get_sentinal_opnfv_role():
533     return OPNFVRole.objects.get_or_create(name="deleted", description="Role was deleted.")
534
535
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)
540
541
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)
549
550     def __str__(self):
551         return self.mac_address + " on host " + str(self.profile.host.name)
552
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)
557
558     def save(self, *args, **kwargs):
559         self.full_clean()
560         super().save(*args, **kwargs)
561
562
563 """
564 Some Enums for dealing with global constants.
565 """
566
567
568 class OPNFV_SETTINGS():
569     """This is a static configuration class."""
570
571     # all the required network types in PDF/IDF spec
572     NETWORK_ROLES = ["public", "private", "admin", "mgmt"]
573
574
575 class ConfigState:
576     NEW = 0
577     RESET = 100
578     CLEAN = 200
579
580
581 RESOURCE_TYPES = [Server]
582
583
584 class ResourceQuery(AbstractModelQuery):
585     model_list = [Server]