Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ db66d048

History | View | Annotate | Download (17.6 kB)

1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions
6
# are met:
7
#
8
#   1. Redistributions of source code must retain the above copyright
9
#      notice, this list of conditions and the following disclaimer.
10
#
11
#  2. Redistributions in binary form must reproduce the above copyright
12
#     notice, this list of conditions and the following disclaimer in the
13
#     documentation and/or other materials provided with the distribution.
14
#
15
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
16
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
19
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25
# SUCH DAMAGE.
26
#
27
# The views and conclusions contained in the software and documentation are
28
# those of the authors and should not be interpreted as representing official
29
# policies, either expressed or implied, of GRNET S.A.
30

    
31

    
32
from django.conf import settings
33
from django.db import models
34

    
35
import datetime
36

    
37
class SynnefoUser(models.Model):
38

    
39
    #TODO: Amend this when we have groups
40
    ACCOUNT_TYPE = (
41
        ('STUDENT', 'Student'),
42
        ('PROFESSOR', 'Professor'),
43
        ('USER', 'Generic User'),
44
        ('HELPDESK', 'Helpdesk User')
45
    )
46

    
47
    name = models.CharField('Synnefo Username', max_length=255, default='')
48
    realname = models.CharField('Real Name', max_length=255, default='')
49
    uniq = models.CharField('External Unique ID', max_length=255,null=True)
50
    credit = models.IntegerField('Credit Balance')
51
    auth_token = models.CharField('Authentication Token', max_length=32, null=True)
52
    auth_token_created = models.DateTimeField('Time of auth token creation', auto_now_add=True, null = True)
53
    auth_token_expires = models.DateTimeField('Time of auth token expiration', auto_now_add=True, null = True)
54
    tmp_auth_token = models.CharField('Temporary authentication token', max_length=32, null=True)
55
    tmp_auth_token_expires = models.DateTimeField('Time of temporary auth token expiration', auto_now_add=True, null = True)
56
    type = models.CharField('Current Image State', choices=ACCOUNT_TYPE, max_length=30)
57
    created = models.DateTimeField('Time of creation', auto_now_add=True)
58
    updated = models.DateTimeField('Time of last update', auto_now=True)
59
    max_invitations = models.IntegerField('Max number of invitations',
60
                                          null=True)
61

    
62
    class Meta:
63
        verbose_name = u'Synnefo User'
64

    
65
    def __unicode__(self):
66
        return self.name
67

    
68
    def get_limit(self, limit_name):
69
        """Returns the limit value for the specified limit"""
70
        limit_objs = Limit.objects.filter(name=limit_name, user=self)
71
        if len(limit_objs) == 1:
72
            return limit_objs[0].value
73
        
74
        return 0
75
        
76
    def _get_credit_quota(self):
77
        """Internal getter function for credit quota"""
78
        return self.get_limit('QUOTA_CREDIT')
79
        
80
    credit_quota = property(_get_credit_quota)
81
    
82
    def _get_monthly_rate(self):
83
        """Internal getter function for monthly credit issue rate"""
84
        return self.get_limit('MONTHLY_RATE')
85
        
86
    monthly_rate = property(_get_monthly_rate)
87
    
88
    def _get_min_credits(self):
89
        """Internal getter function for maximum number of violations"""
90
        return self.get_limit('MIN_CREDITS')
91
        
92
    min_credits = property(_get_min_credits)
93

    
94

    
95
class Image(models.Model):
96
    # This is WIP, FIXME
97
    IMAGE_STATES = (
98
        ('ACTIVE', 'Active'),
99
        ('SAVING', 'Saving'),
100
        ('DELETED', 'Deleted')
101
    )
102

    
103
    # The list of supported Image formats
104
    FORMATS = (
105
        ('dump', 'ext3 dump'),
106
        ('lvm', 'lvm snapshot'),
107
        ('ntfsclone', 'Windows Image produced by ntfsclone')
108
    )
109

    
110
    name = models.CharField('Image name', max_length=255)
111
    state = models.CharField('Current Image State', choices=IMAGE_STATES,
112
                                max_length=30)
113
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
114
    created = models.DateTimeField('Time of creation', auto_now_add=True)
115
    updated = models.DateTimeField('Time of last update', auto_now=True)
116
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
117
    backend_id = models.CharField(max_length=50, default='debian_base')
118
    format = models.CharField(choices=FORMATS, max_length=30, default='dump')
119
    public = models.BooleanField(default=False)
120

    
121
    class Meta:
122
        verbose_name = u'Image'
123

    
124
    def __unicode__(self):
125
        return u'%s' % ( self.name, )
126

    
127

    
128
class ImageMetadata(models.Model):
129
    meta_key = models.CharField('Image metadata key name', max_length=50)
130
    meta_value = models.CharField('Image metadata value', max_length=500)
131
    image = models.ForeignKey(Image)
132
    
133
    class Meta:
134
        verbose_name = u'Key-value pair of Image metadata.'
135
    
136
    def __unicode__(self):
137
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
138

    
139

    
140
class Limit(models.Model):
141
    LIMITS = (
142
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
143
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
144
        ('MONTHLY_RATE', 'Monthly credit issue rate')
145
    )
146
    user = models.ForeignKey(SynnefoUser)
147
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
148
    value = models.IntegerField('Limit current value')
149
    
150
    class Meta:
151
        verbose_name = u'Enforced limit for user'
152
    
153
    def __unicode__(self):
154
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
155

    
156

    
157
class Flavor(models.Model):
158
    cpu = models.IntegerField('Number of CPUs', default=0)
159
    ram = models.IntegerField('Size of RAM', default=0)             # Size in MiB
160
    disk = models.IntegerField('Size of Disk space', default=0)     # Size in GiB
161
    
162
    class Meta:
163
        verbose_name = u'Virtual machine flavor'
164
        unique_together = ("cpu","ram","disk")
165
            
166
    def _get_name(self):
167
        """Returns flavor name (generated)"""
168
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
169

    
170
    def _current_cost(self, active):
171
        """Returns active/inactive cost value
172

173
        set active = True to get active cost and False for the inactive.
174

175
        """
176
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
177
        if len(fch_list) > 0:
178
            if active:
179
                return fch_list[0].cost_active
180
            else:
181
                return fch_list[0].cost_inactive
182

    
183
        return 0
184

    
185
    def _current_cost_active(self):
186
        """Returns current active cost (property method)"""
187
        return self._current_cost(True)
188

    
189
    def _current_cost_inactive(self):
190
        """Returns current inactive cost (property method)"""
191
        return self._current_cost(False)
192

    
193
    name = property(_get_name)
194
    current_cost_active = property(_current_cost_active)
195
    current_cost_inactive = property(_current_cost_inactive)
196

    
197
    def __unicode__(self):
198
        return self.name
199

    
200

    
201
class FlavorCost(models.Model):
202
    cost_active = models.PositiveIntegerField('Active Cost')
203
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
204
    effective_from = models.DateTimeField()
205
    flavor = models.ForeignKey(Flavor)
206
    
207
    class Meta:
208
        verbose_name = u'Pricing history for flavors'
209
    
210
    def __unicode__(self):
211
        return u'Costs (up, down)=(%d, %d) for %s since %s' % \
212
                (int(self.cost_active), int(self.cost_inactive),
213
                 self.flavor.name, self.effective_from)
214

    
215

    
216
class VirtualMachine(models.Model):
217
    # The list of possible actions for a VM
218
    ACTIONS = (
219
       ('CREATE', 'Create VM'),
220
       ('START', 'Start VM'),
221
       ('STOP', 'Shutdown VM'),
222
       ('SUSPEND', 'Admin Suspend VM'),
223
       ('REBOOT', 'Reboot VM'),
224
       ('DESTROY', 'Destroy VM')
225
    )
226
    # The internal operating state of a VM
227
    OPER_STATES = (
228
        ('BUILD', 'Queued for creation'),
229
        ('ERROR', 'Creation failed'),
230
        ('STOPPED', 'Stopped'),
231
        ('STARTED', 'Started'),
232
        ('DESTROYED', 'Destroyed')
233
    )
234
    # The list of possible operations on the backend
235
    BACKEND_OPCODES = (
236
        ('OP_INSTANCE_CREATE', 'Create Instance'),
237
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
238
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
239
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
240
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
241

    
242
        # These are listed here for completeness,
243
        # and are ignored for the time being
244
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
245
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
246
    )
247
    # A backend job may be in one of the following possible states
248
    BACKEND_STATUSES = (
249
        ('queued', 'request queued'),
250
        ('waiting', 'request waiting for locks'),
251
        ('canceling', 'request being canceled'),
252
        ('running', 'request running'),
253
        ('canceled', 'request canceled'),
254
        ('success', 'request completed successfully'),
255
        ('error', 'request returned error')
256
    )
257

    
258
    # The operating state of a VM,
259
    # upon the successful completion of a backend operation.
260
    OPER_STATE_FROM_OPCODE = {
261
        'OP_INSTANCE_CREATE': 'STARTED',
262
        'OP_INSTANCE_REMOVE': 'DESTROYED',
263
        'OP_INSTANCE_STARTUP': 'STARTED',
264
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
265
        'OP_INSTANCE_REBOOT': 'STARTED',
266
        'OP_INSTANCE_SET_PARAMS': None,
267
        'OP_INSTANCE_QUERY_DATA': None
268
    }
269

    
270
    # This dictionary contains the correspondence between
271
    # internal operating states and Server States as defined
272
    # by the Rackspace API.
273
    RSAPI_STATE_FROM_OPER_STATE = {
274
        "BUILD": "BUILD",
275
        "ERROR": "ERROR",
276
        "STOPPED": "STOPPED",
277
        "STARTED": "ACTIVE",
278
        "DESTROYED": "DELETED"
279
    }
280

    
281
    name = models.CharField('Virtual Machine Name', max_length=255)
282
    owner = models.ForeignKey(SynnefoUser)
283
    created = models.DateTimeField(auto_now_add=True)
284
    updated = models.DateTimeField(auto_now=True)
285
    charged = models.DateTimeField(default=datetime.datetime.now())
286
    sourceimage = models.ForeignKey("Image", null=False) 
287
    hostid = models.CharField(max_length=100)
288
    flavor = models.ForeignKey(Flavor)
289
    deleted = models.BooleanField('Deleted', default=False)
290
    suspended = models.BooleanField('Administratively Suspended', default=False)
291

    
292
    # VM State 
293
    # The following fields are volatile data, in the sense
294
    # that they need not be persistent in the DB, but rather
295
    # get generated at runtime by quering Ganeti and applying
296
    # updates received from Ganeti.
297
    
298
    # In the future they could be moved to a separate caching layer
299
    # and removed from the database.
300
    # [vkoukis] after discussion with [faidon].
301
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
302
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
303
    backendjobid = models.PositiveIntegerField(null=True)
304
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
305
                                     null=True)
306
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30,
307
                                        null=True)
308
    backendlogmsg = models.TextField(null=True)
309

    
310
    # Error classes
311
    class InvalidBackendIdError(Exception):
312
         def __init__(self, value):
313
            self.value = value
314
         def __str__(self):
315
            return repr(self.value)
316

    
317
    class InvalidBackendMsgError(Exception):
318
         def __init__(self, opcode, status):
319
            self.opcode = opcode
320
            self.status = status
321
         def __str__(self):
322
            return repr("<opcode: %s, status: %s>" % (str(self.opcode),
323
                                                      str(self.status)))
324

    
325
    class InvalidActionError(Exception):
326
         def __init__(self, action):
327
            self._action = action
328
         def __str__(self):
329
            return repr(str(self._action))
330
    
331
    class DeletedError(Exception):
332
        pass
333
    
334
    class BuildingError(Exception):
335
        pass
336
    
337
    def __init__(self, *args, **kw):
338
        """Initialize state for just created VM instances."""
339
        super(VirtualMachine, self).__init__(*args, **kw)
340
        # This gets called BEFORE an instance gets save()d for
341
        # the first time.
342
        if not self.pk: 
343
            self.action = None
344
            self.backendjobid = None
345
            self.backendjobstatus = None
346
            self.backendopcode = None
347
            self.backendlogmsg = None
348
            self.operstate = 'BUILD'
349

    
350
    def _get_backend_id(self):
351
        """Returns the backend id for this VM by prepending backend-prefix."""
352
        if not self.id:
353
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
354
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
355

    
356
    backend_id = property(_get_backend_id)
357

    
358
    class Meta:
359
        verbose_name = u'Virtual machine instance'
360
        get_latest_by = 'created'
361
    
362
    def __unicode__(self):
363
        return self.name
364

    
365

    
366
class VirtualMachineGroup(models.Model):
367
    """Groups of VMs for SynnefoUsers"""
368
    name = models.CharField(max_length=255)
369
    created = models.DateTimeField('Time of creation', auto_now_add=True)
370
    updated = models.DateTimeField('Time of last update', auto_now=True)
371
    owner = models.ForeignKey(SynnefoUser)
372
    machines = models.ManyToManyField(VirtualMachine)
373

    
374
    class Meta:
375
        verbose_name = u'Virtual Machine Group'
376
        verbose_name_plural = 'Virtual Machine Groups'
377
        ordering = ['name']
378
    
379
    def __unicode__(self):
380
        return self.name
381

    
382

    
383
class VirtualMachineMetadata(models.Model):
384
    meta_key = models.CharField(max_length=50)
385
    meta_value = models.CharField(max_length=500)
386
    vm = models.ForeignKey(VirtualMachine)
387
    
388
    class Meta:
389
        verbose_name = u'Key-value pair of metadata for a VM.'
390
    
391
    def __unicode__(self):
392
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
393

    
394

    
395
class Debit(models.Model):
396
    when = models.DateTimeField()
397
    user = models.ForeignKey(SynnefoUser)
398
    vm = models.ForeignKey(VirtualMachine)
399
    description = models.TextField()
400
    
401
    class Meta:
402
        verbose_name = u'Accounting log'
403

    
404
    def __unicode__(self):
405
        return u'%s - %s - %s - %s' % (self.user.id, self.vm.name,
406
                                       str(self.when), self.description)
407

    
408

    
409
class Disk(models.Model):
410
    name = models.CharField(max_length=255)
411
    created = models.DateTimeField('Time of creation', auto_now_add=True)
412
    updated = models.DateTimeField('Time of last update', auto_now=True)
413
    size = models.PositiveIntegerField('Disk size in GBs')
414
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
415
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
416

    
417
    class Meta:
418
        verbose_name = u'Disk instance'
419

    
420
    def __unicode__(self):
421
        return self.name
422

    
423

    
424
class Network(models.Model):
425
    NETWORK_STATES = (
426
        ('ACTIVE', 'Active'),
427
        ('DELETED', 'Deleted')
428
    )
429
    
430
    name = models.CharField(max_length=255)
431
    created = models.DateTimeField(auto_now_add=True)
432
    updated = models.DateTimeField(auto_now=True)
433
    owner = models.ForeignKey(SynnefoUser, null=True)
434
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
435
    public = models.BooleanField(default=False)
436
    link = models.ForeignKey('NetworkLink', related_name='+')
437
    machines = models.ManyToManyField(VirtualMachine,
438
                                        through='NetworkInterface')
439
    
440
    def __unicode__(self):
441
        return self.name
442

    
443

    
444
class Invitations(models.Model):
445
    source = models.ForeignKey(SynnefoUser, related_name="source")
446
    target = models.ForeignKey(SynnefoUser, related_name="target")
447
    accepted = models.BooleanField('Is the invitation accepted?', default=False)
448
    created = models.DateTimeField(auto_now_add=True)
449
    updated = models.DateTimeField(auto_now=True)
450
    level = models.IntegerField('Invitation depth level', null=True)
451

    
452
    class Meta:
453
        verbose_name = u'Invitation'
454

    
455
    def __unicode__(self):
456
        return "From: %s, To: %s" % (self.source, self.target)
457

    
458

    
459
class NetworkInterface(models.Model):
460
    FIREWALL_PROFILES = (
461
        ('ENABLED', 'Enabled'),
462
        ('DISABLED', 'Disabled'),
463
        ('PROTECTED', 'Protected')
464
    )
465
    
466
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
467
    network = models.ForeignKey(Network, related_name='nics')
468
    created = models.DateTimeField(auto_now_add=True)
469
    updated = models.DateTimeField(auto_now=True)
470
    index = models.IntegerField(null=True)
471
    mac = models.CharField(max_length=17, null=True)
472
    ipv4 = models.CharField(max_length=15, null=True)
473
    ipv6 = models.CharField(max_length=100, null=True)
474
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
475
                                        max_length=30, null=True)
476
    
477
    def __unicode__(self):
478
        return '%s@%s' % (self.machine.name, self.network.name)
479

    
480

    
481
class NetworkLink(models.Model):
482
    network = models.ForeignKey(Network, null=True, related_name='+')
483
    index = models.IntegerField()
484
    name = models.CharField(max_length=255)
485
    available = models.BooleanField(default=True)
486
    
487
    def __unicode__(self):
488
        return self.name