Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 7cc3c7d9

History | View | Annotate | Download (18.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
        ('ADMIN', 'Admin User')
46
    )
47
    
48
    ACCOUNT_STATE = (
49
        ('ACTIVE', 'Active'),
50
        ('DELETED', 'Deleted'),
51
        ('SUSPENDED', 'Suspended')
52
    )
53
    
54
    name = models.CharField('Synnefo Username', max_length=255, default='')
55
    realname = models.CharField('Real Name', max_length=255, default='')
56
    uniq = models.CharField('External Unique ID', max_length=255,null=True)
57
    credit = models.IntegerField('Credit Balance')
58
    auth_token = models.CharField('Authentication Token', max_length=32, null=True)
59
    auth_token_created = models.DateTimeField('Time of auth token creation', auto_now_add=True, null = True)
60
    auth_token_expires = models.DateTimeField('Time of auth token expiration', auto_now_add=True, null = True)
61
    tmp_auth_token = models.CharField('Temporary authentication token', max_length=32, null=True)
62
    tmp_auth_token_expires = models.DateTimeField('Time of temporary auth token expiration', auto_now_add=True, null = True)
63
    type = models.CharField('Account type', choices=ACCOUNT_TYPE,
64
                                max_length=30)
65
    state = models.CharField('Account state', choices=ACCOUNT_STATE,
66
                                max_length=30, default='ACTIVE')
67
    created = models.DateTimeField('Time of creation', auto_now_add=True)
68
    updated = models.DateTimeField('Time of last update', auto_now=True)
69
    max_invitations = models.IntegerField('Max number of invitations',
70
                                          null=True)
71

    
72
    class Meta:
73
        verbose_name = u'Synnefo User'
74

    
75
    def __unicode__(self):
76
        return self.name
77

    
78
    def get_limit(self, limit_name):
79
        """Returns the limit value for the specified limit"""
80
        limit_objs = Limit.objects.filter(name=limit_name, user=self)
81
        if len(limit_objs) == 1:
82
            return limit_objs[0].value
83
        
84
        return 0
85
        
86
    def _get_credit_quota(self):
87
        """Internal getter function for credit quota"""
88
        return self.get_limit('QUOTA_CREDIT')
89
        
90
    credit_quota = property(_get_credit_quota)
91
    
92
    def _get_monthly_rate(self):
93
        """Internal getter function for monthly credit issue rate"""
94
        return self.get_limit('MONTHLY_RATE')
95
        
96
    monthly_rate = property(_get_monthly_rate)
97
    
98
    def _get_min_credits(self):
99
        """Internal getter function for maximum number of violations"""
100
        return self.get_limit('MIN_CREDITS')
101
        
102
    min_credits = property(_get_min_credits)
103

    
104

    
105
class Image(models.Model):
106
    # This is WIP, FIXME
107
    IMAGE_STATES = (
108
        ('ACTIVE', 'Active'),
109
        ('SAVING', 'Saving'),
110
        ('DELETED', 'Deleted')
111
    )
112

    
113
    # The list of supported Image formats
114
    FORMATS = (
115
        ('dump', 'ext3 dump'),
116
        ('extdump', 'Raw ext2/3/4 dump'),
117
        ('lvm', 'lvm snapshot'),
118
        ('ntfsclone', 'Windows Image produced by ntfsclone'),
119
        ('ntfsdump', 'Raw NTFS dump')
120
    )
121

    
122
    name = models.CharField('Image name', max_length=255)
123
    state = models.CharField('Current Image State', choices=IMAGE_STATES,
124
                                max_length=30)
125
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
126
    created = models.DateTimeField('Time of creation', auto_now_add=True)
127
    updated = models.DateTimeField('Time of last update', auto_now=True)
128
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
129
    backend_id = models.CharField(max_length=50, default='debian_base')
130
    format = models.CharField(choices=FORMATS, max_length=30, default='dump')
131
    public = models.BooleanField(default=False)
132

    
133
    class Meta:
134
        verbose_name = u'Image'
135

    
136
    def __unicode__(self):
137
        return u'%s' % ( self.name, )
138

    
139

    
140
class ImageMetadata(models.Model):
141
    meta_key = models.CharField('Image metadata key name', max_length=50)
142
    meta_value = models.CharField('Image metadata value', max_length=500)
143
    image = models.ForeignKey(Image, related_name='metadata')
144
    
145
    class Meta:
146
        unique_together = (('meta_key', 'image'),)
147
        verbose_name = u'Key-value pair of Image metadata.'
148
    
149
    def __unicode__(self):
150
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
151

    
152

    
153
class Limit(models.Model):
154
    LIMITS = (
155
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
156
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
157
        ('MONTHLY_RATE', 'Monthly credit issue rate')
158
    )
159
    user = models.ForeignKey(SynnefoUser)
160
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
161
    value = models.IntegerField('Limit current value')
162
    
163
    class Meta:
164
        verbose_name = u'Enforced limit for user'
165
    
166
    def __unicode__(self):
167
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
168

    
169

    
170
class Flavor(models.Model):
171
    cpu = models.IntegerField('Number of CPUs', default=0)
172
    ram = models.IntegerField('Size of RAM', default=0)             # Size in MiB
173
    disk = models.IntegerField('Size of Disk space', default=0)     # Size in GiB
174
    deleted = models.BooleanField('Deleted', default=False)
175
    
176
    class Meta:
177
        verbose_name = u'Virtual machine flavor'
178
        unique_together = ("cpu","ram","disk")
179
            
180
    def _get_name(self):
181
        """Returns flavor name (generated)"""
182
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
183

    
184
    def _current_cost(self, active):
185
        """Returns active/inactive cost value
186

187
        set active = True to get active cost and False for the inactive.
188

189
        """
190
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
191
        if len(fch_list) > 0:
192
            if active:
193
                return fch_list[0].cost_active
194
            else:
195
                return fch_list[0].cost_inactive
196

    
197
        return 0
198

    
199
    def _current_cost_active(self):
200
        """Returns current active cost (property method)"""
201
        return self._current_cost(True)
202

    
203
    def _current_cost_inactive(self):
204
        """Returns current inactive cost (property method)"""
205
        return self._current_cost(False)
206

    
207
    name = property(_get_name)
208
    current_cost_active = property(_current_cost_active)
209
    current_cost_inactive = property(_current_cost_inactive)
210

    
211
    def __unicode__(self):
212
        return self.name
213

    
214

    
215
class FlavorCost(models.Model):
216
    cost_active = models.PositiveIntegerField('Active Cost')
217
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
218
    effective_from = models.DateTimeField()
219
    flavor = models.ForeignKey(Flavor)
220
    
221
    class Meta:
222
        verbose_name = u'Pricing history for flavors'
223
    
224
    def __unicode__(self):
225
        return u'Costs (up, down)=(%d, %d) for %s since %s' % \
226
                (int(self.cost_active), int(self.cost_inactive),
227
                 self.flavor.name, self.effective_from)
228

    
229

    
230
class VirtualMachine(models.Model):
231
    # The list of possible actions for a VM
232
    ACTIONS = (
233
       ('CREATE', 'Create VM'),
234
       ('START', 'Start VM'),
235
       ('STOP', 'Shutdown VM'),
236
       ('SUSPEND', 'Admin Suspend VM'),
237
       ('REBOOT', 'Reboot VM'),
238
       ('DESTROY', 'Destroy VM')
239
    )
240
    # The internal operating state of a VM
241
    OPER_STATES = (
242
        ('BUILD', 'Queued for creation'),
243
        ('ERROR', 'Creation failed'),
244
        ('STOPPED', 'Stopped'),
245
        ('STARTED', 'Started'),
246
        ('DESTROYED', 'Destroyed')
247
    )
248
    # The list of possible operations on the backend
249
    BACKEND_OPCODES = (
250
        ('OP_INSTANCE_CREATE', 'Create Instance'),
251
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
252
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
253
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
254
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
255

    
256
        # These are listed here for completeness,
257
        # and are ignored for the time being
258
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
259
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
260
    )
261
    # A backend job may be in one of the following possible states
262
    BACKEND_STATUSES = (
263
        ('queued', 'request queued'),
264
        ('waiting', 'request waiting for locks'),
265
        ('canceling', 'request being canceled'),
266
        ('running', 'request running'),
267
        ('canceled', 'request canceled'),
268
        ('success', 'request completed successfully'),
269
        ('error', 'request returned error')
270
    )
271

    
272
    # The operating state of a VM,
273
    # upon the successful completion of a backend operation.
274
    OPER_STATE_FROM_OPCODE = {
275
        'OP_INSTANCE_CREATE': 'STARTED',
276
        'OP_INSTANCE_REMOVE': 'DESTROYED',
277
        'OP_INSTANCE_STARTUP': 'STARTED',
278
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
279
        'OP_INSTANCE_REBOOT': 'STARTED',
280
        'OP_INSTANCE_SET_PARAMS': None,
281
        'OP_INSTANCE_QUERY_DATA': None,
282
        'OP_INSTANCE_REINSTALL' : None,
283
        'OP_INSTANCE_ACTIVATE_DISKS' : None,
284
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
285
        'OP_INSTANCE_REPLACE_DISKS' : None,
286
        'OP_INSTANCE_MIGRATE': None,
287
        'OP_INSTANCE_CONSOLE': None,
288
        'OP_INSTANCE_RECREATE_DISKS': None
289
    }
290

    
291
    # This dictionary contains the correspondence between
292
    # internal operating states and Server States as defined
293
    # by the Rackspace API.
294
    RSAPI_STATE_FROM_OPER_STATE = {
295
        "BUILD": "BUILD",
296
        "ERROR": "ERROR",
297
        "STOPPED": "STOPPED",
298
        "STARTED": "ACTIVE",
299
        "DESTROYED": "DELETED"
300
    }
301

    
302
    name = models.CharField('Virtual Machine Name', max_length=255)
303
    owner = models.ForeignKey(SynnefoUser)
304
    created = models.DateTimeField(auto_now_add=True)
305
    updated = models.DateTimeField(auto_now=True)
306
    charged = models.DateTimeField(default=datetime.datetime.now())
307
    sourceimage = models.ForeignKey("Image", null=False) 
308
    hostid = models.CharField(max_length=100)
309
    flavor = models.ForeignKey(Flavor)
310
    deleted = models.BooleanField('Deleted', default=False)
311
    suspended = models.BooleanField('Administratively Suspended', default=False)
312

    
313
    # VM State 
314
    # The following fields are volatile data, in the sense
315
    # that they need not be persistent in the DB, but rather
316
    # get generated at runtime by quering Ganeti and applying
317
    # updates received from Ganeti.
318
    
319
    # In the future they could be moved to a separate caching layer
320
    # and removed from the database.
321
    # [vkoukis] after discussion with [faidon].
322
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
323
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
324
    backendjobid = models.PositiveIntegerField(null=True)
325
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
326
                                     null=True)
327
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30,
328
                                        null=True)
329
    backendlogmsg = models.TextField(null=True)
330
    buildpercentage = models.IntegerField(default=0)
331

    
332
    # Error classes
333
    class InvalidBackendIdError(Exception):
334
         def __init__(self, value):
335
            self.value = value
336
         def __str__(self):
337
            return repr(self.value)
338

    
339
    class InvalidBackendMsgError(Exception):
340
         def __init__(self, opcode, status):
341
            self.opcode = opcode
342
            self.status = status
343
         def __str__(self):
344
            return repr("<opcode: %s, status: %s>" % (str(self.opcode),
345
                                                      str(self.status)))
346

    
347
    class InvalidActionError(Exception):
348
         def __init__(self, action):
349
            self._action = action
350
         def __str__(self):
351
            return repr(str(self._action))
352
    
353
    class DeletedError(Exception):
354
        pass
355
    
356
    class BuildingError(Exception):
357
        pass
358

    
359
    #class IllegalState(Exception):
360
    #    pass
361
    
362
    def __init__(self, *args, **kw):
363
        """Initialize state for just created VM instances."""
364
        super(VirtualMachine, self).__init__(*args, **kw)
365
        # This gets called BEFORE an instance gets save()d for
366
        # the first time.
367
        if not self.pk: 
368
            self.action = None
369
            self.backendjobid = None
370
            self.backendjobstatus = None
371
            self.backendopcode = None
372
            self.backendlogmsg = None
373
            self.operstate = 'BUILD'
374

    
375
    def _get_backend_id(self):
376
        """Returns the backend id for this VM by prepending backend-prefix."""
377
        if not self.id:
378
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
379
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
380

    
381
    backend_id = property(_get_backend_id)
382

    
383
    class Meta:
384
        verbose_name = u'Virtual machine instance'
385
        get_latest_by = 'created'
386
    
387
    def __unicode__(self):
388
        return self.name
389

    
390

    
391
class VirtualMachineGroup(models.Model):
392
    """Groups of VMs for SynnefoUsers"""
393
    name = models.CharField(max_length=255)
394
    created = models.DateTimeField('Time of creation', auto_now_add=True)
395
    updated = models.DateTimeField('Time of last update', auto_now=True)
396
    owner = models.ForeignKey(SynnefoUser)
397
    machines = models.ManyToManyField(VirtualMachine)
398

    
399
    class Meta:
400
        verbose_name = u'Virtual Machine Group'
401
        verbose_name_plural = 'Virtual Machine Groups'
402
        ordering = ['name']
403
    
404
    def __unicode__(self):
405
        return self.name
406

    
407

    
408
class VirtualMachineMetadata(models.Model):
409
    meta_key = models.CharField(max_length=50)
410
    meta_value = models.CharField(max_length=500)
411
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
412
    
413
    class Meta:
414
        unique_together = (('meta_key', 'vm'),)
415
        verbose_name = u'Key-value pair of metadata for a VM.'
416
    
417
    def __unicode__(self):
418
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
419

    
420

    
421
class Debit(models.Model):
422
    when = models.DateTimeField()
423
    user = models.ForeignKey(SynnefoUser)
424
    vm = models.ForeignKey(VirtualMachine)
425
    description = models.TextField()
426
    
427
    class Meta:
428
        verbose_name = u'Accounting log'
429

    
430
    def __unicode__(self):
431
        return u'%s - %s - %s - %s' % (self.user.id, self.vm.name,
432
                                       str(self.when), self.description)
433

    
434

    
435
class Disk(models.Model):
436
    name = models.CharField(max_length=255)
437
    created = models.DateTimeField('Time of creation', auto_now_add=True)
438
    updated = models.DateTimeField('Time of last update', auto_now=True)
439
    size = models.PositiveIntegerField('Disk size in GBs')
440
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
441
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
442

    
443
    class Meta:
444
        verbose_name = u'Disk instance'
445

    
446
    def __unicode__(self):
447
        return self.name
448

    
449

    
450
class Network(models.Model):
451
    NETWORK_STATES = (
452
        ('ACTIVE', 'Active'),
453
        ('DELETED', 'Deleted')
454
    )
455
    
456
    name = models.CharField(max_length=255)
457
    created = models.DateTimeField(auto_now_add=True)
458
    updated = models.DateTimeField(auto_now=True)
459
    owner = models.ForeignKey(SynnefoUser, null=True)
460
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
461
    public = models.BooleanField(default=False)
462
    link = models.ForeignKey('NetworkLink', related_name='+')
463
    machines = models.ManyToManyField(VirtualMachine,
464
                                        through='NetworkInterface')
465
    
466
    def __unicode__(self):
467
        return self.name
468

    
469

    
470
class Invitations(models.Model):
471
    source = models.ForeignKey(SynnefoUser, related_name="source")
472
    target = models.ForeignKey(SynnefoUser, related_name="target")
473
    accepted = models.BooleanField('Is the invitation accepted?', default=False)
474
    created = models.DateTimeField(auto_now_add=True)
475
    updated = models.DateTimeField(auto_now=True)
476
    level = models.IntegerField('Invitation depth level', null=True)
477

    
478
    class Meta:
479
        verbose_name = u'Invitation'
480

    
481
    def __unicode__(self):
482
        return "From: %s, To: %s" % (self.source, self.target)
483

    
484

    
485
class NetworkInterface(models.Model):
486
    FIREWALL_PROFILES = (
487
        ('ENABLED', 'Enabled'),
488
        ('DISABLED', 'Disabled'),
489
        ('PROTECTED', 'Protected')
490
    )
491
    
492
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
493
    network = models.ForeignKey(Network, related_name='nics')
494
    created = models.DateTimeField(auto_now_add=True)
495
    updated = models.DateTimeField(auto_now=True)
496
    index = models.IntegerField(null=True)
497
    mac = models.CharField(max_length=17, null=True)
498
    ipv4 = models.CharField(max_length=15, null=True)
499
    ipv6 = models.CharField(max_length=100, null=True)
500
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
501
                                        max_length=30, null=True)
502
    
503
    def __unicode__(self):
504
        return '%s@%s' % (self.machine.name, self.network.name)
505

    
506

    
507
class NetworkLink(models.Model):
508
    network = models.ForeignKey(Network, null=True, related_name='+')
509
    index = models.IntegerField()
510
    name = models.CharField(max_length=255)
511
    available = models.BooleanField(default=True)
512
    
513
    def __unicode__(self):
514
        return self.name