1 ##############################################################################
2 # Copyright (c) 2016 Max Breitenfeldt and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
11 from django.contrib.auth.models import User
12 from django.db import models
13 from django.apps import apps
17 from collections import Counter
19 from dashboard.exceptions import ResourceAvailabilityException
22 class LabStatus(object):
24 A Poor man's enum for the status of a lab.
26 If everything is working fine at a lab, it is UP.
27 If it is down temporarily e.g. for maintenance, it is TEMP_DOWN
28 If its broken, its DOWN
36 def upload_to(object, filename):
37 return object.user.username + '/' + filename
40 class UserProfile(models.Model):
41 """Extend the Django User model."""
43 user = models.OneToOneField(User, on_delete=models.CASCADE)
44 timezone = models.CharField(max_length=100, blank=False, default='UTC')
45 ssh_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
46 pgp_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
47 email_addr = models.CharField(max_length=300, blank=False, default='email@mail.com')
48 company = models.CharField(max_length=200, blank=False)
50 oauth_token = models.CharField(max_length=1024, blank=False)
51 oauth_secret = models.CharField(max_length=1024, blank=False)
53 jira_url = models.CharField(max_length=100, null=True, blank=True, default='')
55 full_name = models.CharField(max_length=100, null=True, blank=True, default='')
56 booking_privledge = models.BooleanField(default=False)
58 public_user = models.BooleanField(default=False)
61 db_table = 'user_profile'
64 return self.user.username
67 class VlanManager(models.Model):
69 Keeps track of the vlans for a lab.
71 Vlans are represented as indexes into a 4096 element list.
72 This list is serialized to JSON for storing in the DB.
75 # list of length 4096 containing either 0 (not available) or 1 (available)
76 vlans = models.TextField()
77 # list of length 4096 containing either 0 (not reserved) or 1 (reserved)
78 reserved_vlans = models.TextField()
80 block_size = models.IntegerField()
82 # True if the lab allows two different users to have the same private vlans
83 # if they use QinQ or a vxlan overlay, for example
84 allow_overlapping = models.BooleanField()
86 def get_vlans(self, count=1, within=None):
88 Return the IDs of available vlans as a list[int], but does not reserve them.
90 Will throw index exception if not enough vlans are available.
91 Always returns a list of ints
93 If `within` is not none, will filter against that as a set, requiring that any vlans returned are within that set
96 vlans = json.loads(self.vlans)
97 reserved = json.loads(self.reserved_vlans)
99 for i in range(0, len(vlans) - 1):
100 if len(allocated) >= count:
103 if vlans[i] == 0 and self.allow_overlapping is False:
109 # vlan is available and not reserved, so safe to add
110 if within is not None:
117 if len(allocated) != count:
118 raise ResourceAvailabilityException("There were not enough available private vlans for the allocation. Please contact the administrators.")
122 def get_public_vlan(self, within=None):
123 """Return reference to an available public network without reserving it."""
124 r = PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False)
125 if within is not None:
126 r = r.filter(vlan__in=within)
129 raise ResourceAvailabilityException("There were not enough available public vlans for the allocation. Please contact the administrators.")
133 def reserve_public_vlan(self, vlan):
134 """Reserves the Public Network that has the given vlan."""
135 net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False)
139 def release_public_vlan(self, vlan):
140 """Un-reserves a public network with the given vlan."""
141 net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True)
145 def public_vlan_is_available(self, vlan):
147 Whether the public vlan is available.
149 returns true if the network with the given vlan is free to use,
152 net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan)
153 return not net.in_use
155 def is_available(self, vlans):
157 If the vlans are available.
159 'vlans' is either a single vlan id integer or a list of integers
160 will return true (available) or false
162 if self.allow_overlapping:
165 reserved = json.loads(self.reserved_vlans)
166 vlan_master_list = json.loads(self.vlans)
173 if not vlan_master_list[vlan] or reserved[vlan]:
177 def release_vlans(self, vlans):
179 Make the vlans available for another booking.
181 'vlans' is either a single vlan id integer or a list of integers
182 will make the vlans available
183 doesnt return a value
185 my_vlans = json.loads(self.vlans)
194 self.vlans = json.dumps(my_vlans)
197 def reserve_vlans(self, vlans):
199 Reserves all given vlans or throws a ValueError.
201 vlans can be an integer or a list of integers.
203 my_vlans = json.loads(self.vlans)
205 reserved = json.loads(self.reserved_vlans)
215 if my_vlans[vlan] == 0 or reserved[vlan] == 1:
216 raise ValueError("vlan " + str(vlan) + " is not available")
219 self.vlans = json.dumps(my_vlans)
223 class Lab(models.Model):
225 Model representing a Hosting Lab.
227 Anybody that wants to host resources for LaaS needs to have a Lab model
228 We associate hardware with Labs so we know what is available and where.
231 lab_user = models.OneToOneField(User, on_delete=models.CASCADE)
232 name = models.CharField(max_length=200, primary_key=True, unique=True, null=False, blank=False)
233 contact_email = models.EmailField(max_length=200, null=True, blank=True)
234 contact_phone = models.CharField(max_length=20, null=True, blank=True)
235 status = models.IntegerField(default=LabStatus.UP)
236 vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True)
237 location = models.TextField(default="unknown")
238 # This token must apear in API requests from this lab
239 api_token = models.CharField(max_length=50)
240 description = models.CharField(max_length=240)
241 lab_info_link = models.URLField(null=True)
242 project = models.CharField(default='LaaS', max_length=100)
245 def make_api_token():
246 """Generate random 45 character string for API token."""
247 alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
250 key += random.choice(alphabet)
253 def get_available_resources(self):
254 # Cannot import model normally due to ciruclar import
255 Server = apps.get_model('resource_inventory', 'Server') # TODO: Find way to import ResourceQuery
256 resources = [str(resource.profile) for resource in Server.objects.filter(lab=self, working=True, booked=False)]
257 return dict(Counter(resources))
263 class PublicNetwork(models.Model):
264 """L2/L3 network that can reach the internet."""
266 vlan = models.IntegerField()
267 lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
268 in_use = models.BooleanField(default=False)
269 cidr = models.CharField(max_length=50, default="0.0.0.0/0")
270 gateway = models.CharField(max_length=50, default="0.0.0.0")
273 class Downtime(models.Model):
277 Labs can create Downtime objects so the dashboard can
278 alert users that the lab is down, etc
281 start = models.DateTimeField()
282 end = models.DateTimeField()
283 lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
284 description = models.TextField(default="This lab will be down for maintenance")
286 def save(self, *args, **kwargs):
287 if self.start >= self.end:
288 raise ValueError('Start date is after end date')
290 # check for overlapping downtimes
291 overlap_start = Downtime.objects.filter(lab=self.lab, start__gt=self.start, start__lt=self.end).exists()
292 overlap_end = Downtime.objects.filter(lab=self.lab, end__lt=self.end, end__gt=self.start).exists()
294 if overlap_start or overlap_end:
295 raise ValueError('Overlapping Downtime')
297 return super(Downtime, self).save(*args, **kwargs)