1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
|
##############################################################################
# Copyright (c) 2016 Max Breitenfeldt and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Apache License, Version 2.0
# which accompanies this distribution, and is available at
# http://www.apache.org/licenses/LICENSE-2.0
##############################################################################
from django.contrib.auth.models import User
from django.db import models
from django.apps import apps
import json
import random
from collections import Counter
class LabStatus(object):
"""
A Poor man's enum for the status of a lab.
If everything is working fine at a lab, it is UP.
If it is down temporarily e.g. for maintenance, it is TEMP_DOWN
If its broken, its DOWN
"""
UP = 0
TEMP_DOWN = 100
DOWN = 200
def upload_to(object, filename):
return object.user.username + '/' + filename
class UserProfile(models.Model):
"""Extend the Django User model."""
user = models.OneToOneField(User, on_delete=models.CASCADE)
timezone = models.CharField(max_length=100, blank=False, default='UTC')
ssh_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
pgp_public_key = models.FileField(upload_to=upload_to, null=True, blank=True)
email_addr = models.CharField(max_length=300, blank=False, default='email@mail.com')
company = models.CharField(max_length=200, blank=False)
oauth_token = models.CharField(max_length=1024, blank=False)
oauth_secret = models.CharField(max_length=1024, blank=False)
jira_url = models.CharField(max_length=100, default='')
full_name = models.CharField(max_length=100, default='')
booking_privledge = models.BooleanField(default=False)
class Meta:
db_table = 'user_profile'
def __str__(self):
return self.user.username
class VlanManager(models.Model):
"""
Keeps track of the vlans for a lab.
Vlans are represented as indexes into a 4096 element list.
This list is serialized to JSON for storing in the DB.
"""
# list of length 4096 containing either 0 (not available) or 1 (available)
vlans = models.TextField()
# list of length 4096 containing either 0 (not reserved) or 1 (reserved)
reserved_vlans = models.TextField()
block_size = models.IntegerField()
# True if the lab allows two different users to have the same private vlans
# if they use QinQ or a vxlan overlay, for example
allow_overlapping = models.BooleanField()
def get_vlan(self, count=1):
"""
Return the ID of available vlans, but does not reserve them.
Will throw index exception if not enough vlans are available.
If count == 1, the return value is an int. Otherwise, it is a list of ints.
"""
allocated = []
vlans = json.loads(self.vlans)
for i in range(count):
new_vlan = vlans.index(1) # will throw if none available
vlans[new_vlan] = 0
allocated.append(new_vlan)
if count == 1:
return allocated[0]
return allocated
def get_public_vlan(self):
"""Return reference to an available public network without reserving it."""
return PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False).first()
def reserve_public_vlan(self, vlan):
"""Reserves the Public Network that has the given vlan."""
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False)
net.in_use = True
net.save()
def release_public_vlan(self, vlan):
"""Un-reserves a public network with the given vlan."""
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True)
net.in_use = False
net.save()
def public_vlan_is_available(self, vlan):
"""
Whether the public vlan is available.
returns true if the network with the given vlan is free to use,
False otherwise
"""
net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan)
return not net.in_use
def is_available(self, vlans):
"""
If the vlans are available.
'vlans' is either a single vlan id integer or a list of integers
will return true (available) or false
"""
if self.allow_overlapping:
return True
reserved = json.loads(self.reserved_vlans)
vlan_master_list = json.loads(self.vlans)
try:
iter(vlans)
except Exception:
vlans = [vlans]
for vlan in vlans:
if not vlan_master_list[vlan] or reserved[vlan]:
return False
return True
def release_vlans(self, vlans):
"""
Make the vlans available for another booking.
'vlans' is either a single vlan id integer or a list of integers
will make the vlans available
doesnt return a value
"""
my_vlans = json.loads(self.vlans)
try:
iter(vlans)
except Exception:
vlans = [vlans]
for vlan in vlans:
my_vlans[vlan] = 1
self.vlans = json.dumps(my_vlans)
self.save()
def reserve_vlans(self, vlans):
"""
Reserves all given vlans or throws a ValueError.
vlans can be an integer or a list of integers.
"""
my_vlans = json.loads(self.vlans)
try:
iter(vlans)
except Exception:
vlans = [vlans]
vlans = set(vlans)
for vlan in vlans:
if my_vlans[vlan] == 0:
raise ValueError("vlan " + str(vlan) + " is not available")
my_vlans[vlan] = 0
self.vlans = json.dumps(my_vlans)
self.save()
class Lab(models.Model):
"""
Model representing a Hosting Lab.
Anybody that wants to host resources for LaaS needs to have a Lab model
We associate hardware with Labs so we know what is available and where.
"""
lab_user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200, primary_key=True, unique=True, null=False, blank=False)
contact_email = models.EmailField(max_length=200, null=True, blank=True)
contact_phone = models.CharField(max_length=20, null=True, blank=True)
status = models.IntegerField(default=LabStatus.UP)
vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True)
location = models.TextField(default="unknown")
# This token must apear in API requests from this lab
api_token = models.CharField(max_length=50)
description = models.CharField(max_length=240)
@staticmethod
def make_api_token():
"""Generate random 45 character string for API token."""
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
key = ""
for i in range(45):
key += random.choice(alphabet)
return key
def get_available_resources(self):
# Cannot import model normally due to ciruclar import
Server = apps.get_model('resource_inventory', 'Server') # TODO: Find way to import ResourceQuery
resources = [str(resource.profile) for resource in Server.objects.filter(lab=self, booked=False)]
return dict(Counter(resources))
def __str__(self):
return self.name
class PublicNetwork(models.Model):
"""L2/L3 network that can reach the internet."""
vlan = models.IntegerField()
lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
in_use = models.BooleanField(default=False)
cidr = models.CharField(max_length=50, default="0.0.0.0/0")
gateway = models.CharField(max_length=50, default="0.0.0.0")
class Downtime(models.Model):
"""
A Downtime event.
Labs can create Downtime objects so the dashboard can
alert users that the lab is down, etc
"""
start = models.DateTimeField()
end = models.DateTimeField()
lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
description = models.TextField(default="This lab will be down for maintenance")
def save(self, *args, **kwargs):
if self.start >= self.end:
raise ValueError('Start date is after end date')
# check for overlapping downtimes
overlap_start = Downtime.objects.filter(lab=self.lab, start__gt=self.start, start__lt=self.end).exists()
overlap_end = Downtime.objects.filter(lab=self.lab, end__lt=self.end, end__gt=self.start).exists()
if overlap_start or overlap_end:
raise ValueError('Overlapping Downtime')
return super(Downtime, self).save(*args, **kwargs)
|