aboutsummaryrefslogtreecommitdiffstats
path: root/src/account/models.py
blob: 2c133bba0e8906679164767030147ac1bff50f23 (plain)
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
##############################################################################
# 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
from django.core.exceptions import ValidationError
import re
import json
import random

from collections import Counter

from dashboard.exceptions import ResourceAvailabilityException


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, null=True, blank=True, default='')
    full_name = models.CharField(max_length=100, null=True, blank=True, default='')
    booking_privledge = models.BooleanField(default=False)

    class Meta:
        db_table = 'user_profile'

    def clean(self, *args, **kwargs):
        company = self.company
        regex = r'[a-z\_\-\.\$]*'
        pattern = re.compile(regex)

        if not pattern.fullmatch(company):
            raise ValidationError('Company may only include lowercase letters, _, -, . and $')

        super().clean(*args, **kwargs)

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

    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)
        reserved = json.loads(self.reserved_vlans)

        for i in range(0, len(vlans) - 1):
            if len(allocated) >= count:
                break

            if vlans[i] == 0 and self.allow_overlapping is False:
                continue

            if reserved[i] == 1:
                continue

            # vlan is available and not reserved, so safe to add
            allocated.append(i)
            continue

        if len(allocated) != count:
            raise ResourceAvailabilityException("can't allocate the vlans requested")

        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)

        reserved = json.loads(self.reserved_vlans)

        try:
            iter(vlans)
        except Exception:
            vlans = [vlans]

        vlans = set(vlans)

        for vlan in vlans:
            if my_vlans[vlan] == 0 or reserved[vlan] == 1:
                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)
    lab_info_link = models.URLField(null=True)
    project = models.CharField(default='LaaS', max_length=100)

    @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)