add nick
[laas.git] / src / account / models.py
1 ##############################################################################
2 # Copyright (c) 2016 Max Breitenfeldt and others.
3 #
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 ##############################################################################
9
10
11 from django.contrib.auth.models import User
12 from django.db import models
13 from django.apps import apps
14 import json
15 import random
16
17 from collections import Counter
18
19 from dashboard.exceptions import ResourceAvailabilityException
20
21
22 class LabStatus(object):
23     """
24     A Poor man's enum for the status of a lab.
25
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
29     """
30
31     UP = 0
32     TEMP_DOWN = 100
33     DOWN = 200
34
35
36 def upload_to(object, filename):
37     return object.user.username + '/' + filename
38
39
40 class UserProfile(models.Model):
41     """Extend the Django User model."""
42
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)
49
50     oauth_token = models.CharField(max_length=1024, blank=False)
51     oauth_secret = models.CharField(max_length=1024, blank=False)
52
53     jira_url = models.CharField(max_length=100, null=True, blank=True, default='')
54
55     full_name = models.CharField(max_length=100, null=True, blank=True, default='')
56     booking_privledge = models.BooleanField(default=False)
57
58     public_user = models.BooleanField(default=False)
59
60     class Meta:
61         db_table = 'user_profile'
62
63     def __str__(self):
64         return self.user.username
65
66
67 class VlanManager(models.Model):
68     """
69     Keeps track of the vlans for a lab.
70
71     Vlans are represented as indexes into a 4096 element list.
72     This list is serialized to JSON for storing in the DB.
73     """
74
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()
79
80     block_size = models.IntegerField()
81
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()
85
86     def get_vlans(self, count=1, within=None):
87         """
88         Return the IDs of available vlans as a list[int], but does not reserve them.
89
90         Will throw index exception if not enough vlans are available.
91         Always returns a list of ints
92
93         If `within` is not none, will filter against that as a set, requiring that any vlans returned are within that set
94         """
95         allocated = []
96         vlans = json.loads(self.vlans)
97         reserved = json.loads(self.reserved_vlans)
98
99         for i in range(0, len(vlans) - 1):
100             if len(allocated) >= count:
101                 break
102
103             if vlans[i] == 0 and self.allow_overlapping is False:
104                 continue
105
106             if reserved[i] == 1:
107                 continue
108
109             # vlan is available and not reserved, so safe to add
110             if within is not None:
111                 if i in within:
112                     allocated.append(i)
113             else:
114                 allocated.append(i)
115             continue
116
117         if len(allocated) != count:
118             raise ResourceAvailabilityException("There were not enough available private vlans for the allocation. Please contact the administrators.")
119
120         return allocated
121
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)
127
128         if r.count() < 1:
129             raise ResourceAvailabilityException("There were not enough available public vlans for the allocation. Please contact the administrators.")
130
131         return r.first()
132
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)
136         net.in_use = True
137         net.save()
138
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)
142         net.in_use = False
143         net.save()
144
145     def public_vlan_is_available(self, vlan):
146         """
147         Whether the public vlan is available.
148
149         returns true if the network with the given vlan is free to use,
150         False otherwise
151         """
152         net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan)
153         return not net.in_use
154
155     def is_available(self, vlans):
156         """
157         If the vlans are available.
158
159         'vlans' is either a single vlan id integer or a list of integers
160         will return true (available) or false
161         """
162         if self.allow_overlapping:
163             return True
164
165         reserved = json.loads(self.reserved_vlans)
166         vlan_master_list = json.loads(self.vlans)
167         try:
168             iter(vlans)
169         except Exception:
170             vlans = [vlans]
171
172         for vlan in vlans:
173             if not vlan_master_list[vlan] or reserved[vlan]:
174                 return False
175         return True
176
177     def release_vlans(self, vlans):
178         """
179         Make the vlans available for another booking.
180
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
184         """
185         my_vlans = json.loads(self.vlans)
186
187         try:
188             iter(vlans)
189         except Exception:
190             vlans = [vlans]
191
192         for vlan in vlans:
193             my_vlans[vlan] = 1
194         self.vlans = json.dumps(my_vlans)
195         self.save()
196
197     def reserve_vlans(self, vlans):
198         """
199         Reserves all given vlans or throws a ValueError.
200
201         vlans can be an integer or a list of integers.
202         """
203         my_vlans = json.loads(self.vlans)
204
205         reserved = json.loads(self.reserved_vlans)
206
207         try:
208             iter(vlans)
209         except Exception:
210             vlans = [vlans]
211
212         vlans = set(vlans)
213
214         for vlan in vlans:
215             if my_vlans[vlan] == 0 or reserved[vlan] == 1:
216                 raise ValueError("vlan " + str(vlan) + " is not available")
217
218             my_vlans[vlan] = 0
219         self.vlans = json.dumps(my_vlans)
220         self.save()
221
222
223 class Lab(models.Model):
224     """
225     Model representing a Hosting Lab.
226
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.
229     """
230
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)
243
244     @staticmethod
245     def make_api_token():
246         """Generate random 45 character string for API token."""
247         alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
248         key = ""
249         for i in range(45):
250             key += random.choice(alphabet)
251         return key
252
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))
258
259     def __str__(self):
260         return self.name
261
262
263 class PublicNetwork(models.Model):
264     """L2/L3 network that can reach the internet."""
265
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")
271
272
273 class Downtime(models.Model):
274     """
275     A Downtime event.
276
277     Labs can create Downtime objects so the dashboard can
278     alert users that the lab is down, etc
279     """
280
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")
285
286     def save(self, *args, **kwargs):
287         if self.start >= self.end:
288             raise ValueError('Start date is after end date')
289
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()
293
294         if overlap_start or overlap_end:
295             raise ValueError('Overlapping Downtime')
296
297         return super(Downtime, self).save(*args, **kwargs)