Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ c909cbbd

History | View | Annotate | Download (19.3 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('RAM size in MiB', default=0)
173
    disk = models.IntegerField('Disk size in GiB', default=0)
174
    disk_template = models.CharField('Disk template', max_length=32,
175
                                default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
176
    deleted = models.BooleanField('Deleted', default=False)
177
    
178
    class Meta:
179
        verbose_name = u'Virtual machine flavor'
180
        unique_together = ("cpu","ram","disk")
181
            
182
    def _get_name(self):
183
        """Returns flavor name (generated)"""
184
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
185

    
186
    def _current_cost(self, active):
187
        """Returns active/inactive cost value
188

189
        set active = True to get active cost and False for the inactive.
190

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

    
199
        return 0
200

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

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

    
209
    name = property(_get_name)
210
    current_cost_active = property(_current_cost_active)
211
    current_cost_inactive = property(_current_cost_inactive)
212

    
213
    def __unicode__(self):
214
        return self.name
215

    
216

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

    
231

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

    
258
        # These are listed here for completeness,
259
        # and are ignored for the time being
260
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
261
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
262
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
263
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
264
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
265
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
266
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
267
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
268
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
269
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
270
    )
271
    # A backend job may be in one of the following possible states
272
    BACKEND_STATUSES = (
273
        ('queued', 'request queued'),
274
        ('waiting', 'request waiting for locks'),
275
        ('canceling', 'request being canceled'),
276
        ('running', 'request running'),
277
        ('canceled', 'request canceled'),
278
        ('success', 'request completed successfully'),
279
        ('error', 'request returned error')
280
    )
281

    
282
    # The operating state of a VM,
283
    # upon the successful completion of a backend operation.
284
    # IMPORTANT: Make sure all keys have a corresponding
285
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
286
    OPER_STATE_FROM_OPCODE = {
287
        'OP_INSTANCE_CREATE': 'STARTED',
288
        'OP_INSTANCE_REMOVE': 'DESTROYED',
289
        'OP_INSTANCE_STARTUP': 'STARTED',
290
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
291
        'OP_INSTANCE_REBOOT': 'STARTED',
292
        'OP_INSTANCE_SET_PARAMS': None,
293
        'OP_INSTANCE_QUERY_DATA': None,
294
        'OP_INSTANCE_REINSTALL' : None,
295
        'OP_INSTANCE_ACTIVATE_DISKS' : None,
296
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
297
        'OP_INSTANCE_REPLACE_DISKS' : None,
298
        'OP_INSTANCE_MIGRATE': None,
299
        'OP_INSTANCE_CONSOLE': None,
300
        'OP_INSTANCE_RECREATE_DISKS': None,
301
        'OP_INSTANCE_FAILOVER': None
302
    }
303

    
304
    # This dictionary contains the correspondence between
305
    # internal operating states and Server States as defined
306
    # by the Rackspace API.
307
    RSAPI_STATE_FROM_OPER_STATE = {
308
        "BUILD": "BUILD",
309
        "ERROR": "ERROR",
310
        "STOPPED": "STOPPED",
311
        "STARTED": "ACTIVE",
312
        "DESTROYED": "DELETED"
313
    }
314

    
315
    name = models.CharField('Virtual Machine Name', max_length=255)
316
    owner = models.ForeignKey(SynnefoUser)
317
    created = models.DateTimeField(auto_now_add=True)
318
    updated = models.DateTimeField(auto_now=True)
319
    charged = models.DateTimeField(default=datetime.datetime.now())
320
    sourceimage = models.ForeignKey("Image", null=False) 
321
    hostid = models.CharField(max_length=100)
322
    flavor = models.ForeignKey(Flavor)
323
    deleted = models.BooleanField('Deleted', default=False)
324
    suspended = models.BooleanField('Administratively Suspended', default=False)
325

    
326
    # VM State 
327
    # The following fields are volatile data, in the sense
328
    # that they need not be persistent in the DB, but rather
329
    # get generated at runtime by quering Ganeti and applying
330
    # updates received from Ganeti.
331
    
332
    # In the future they could be moved to a separate caching layer
333
    # and removed from the database.
334
    # [vkoukis] after discussion with [faidon].
335
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
336
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
337
    backendjobid = models.PositiveIntegerField(null=True)
338
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
339
                                     null=True)
340
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30,
341
                                        null=True)
342
    backendlogmsg = models.TextField(null=True)
343
    buildpercentage = models.IntegerField(default=0)
344

    
345
    # Error classes
346
    class InvalidBackendIdError(Exception):
347
         def __init__(self, value):
348
            self.value = value
349
         def __str__(self):
350
            return repr(self.value)
351

    
352
    class InvalidBackendMsgError(Exception):
353
         def __init__(self, opcode, status):
354
            self.opcode = opcode
355
            self.status = status
356
         def __str__(self):
357
            return repr("<opcode: %s, status: %s>" % (str(self.opcode),
358
                                                      str(self.status)))
359

    
360
    class InvalidActionError(Exception):
361
         def __init__(self, action):
362
            self._action = action
363
         def __str__(self):
364
            return repr(str(self._action))
365
    
366
    class DeletedError(Exception):
367
        pass
368
    
369
    class BuildingError(Exception):
370
        pass
371

    
372
    #class IllegalState(Exception):
373
    #    pass
374
    
375
    def __init__(self, *args, **kw):
376
        """Initialize state for just created VM instances."""
377
        super(VirtualMachine, self).__init__(*args, **kw)
378
        # This gets called BEFORE an instance gets save()d for
379
        # the first time.
380
        if not self.pk: 
381
            self.action = None
382
            self.backendjobid = None
383
            self.backendjobstatus = None
384
            self.backendopcode = None
385
            self.backendlogmsg = None
386
            self.operstate = 'BUILD'
387

    
388
    def _get_backend_id(self):
389
        """Returns the backend id for this VM by prepending backend-prefix."""
390
        if not self.id:
391
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
392
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
393

    
394
    backend_id = property(_get_backend_id)
395

    
396
    class Meta:
397
        verbose_name = u'Virtual machine instance'
398
        get_latest_by = 'created'
399
    
400
    def __unicode__(self):
401
        return self.name
402

    
403

    
404
class VirtualMachineGroup(models.Model):
405
    """Groups of VMs for SynnefoUsers"""
406
    name = models.CharField(max_length=255)
407
    created = models.DateTimeField('Time of creation', auto_now_add=True)
408
    updated = models.DateTimeField('Time of last update', auto_now=True)
409
    owner = models.ForeignKey(SynnefoUser)
410
    machines = models.ManyToManyField(VirtualMachine)
411

    
412
    class Meta:
413
        verbose_name = u'Virtual Machine Group'
414
        verbose_name_plural = 'Virtual Machine Groups'
415
        ordering = ['name']
416
    
417
    def __unicode__(self):
418
        return self.name
419

    
420

    
421
class VirtualMachineMetadata(models.Model):
422
    meta_key = models.CharField(max_length=50)
423
    meta_value = models.CharField(max_length=500)
424
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
425
    
426
    class Meta:
427
        unique_together = (('meta_key', 'vm'),)
428
        verbose_name = u'Key-value pair of metadata for a VM.'
429
    
430
    def __unicode__(self):
431
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
432

    
433

    
434
class Debit(models.Model):
435
    when = models.DateTimeField()
436
    user = models.ForeignKey(SynnefoUser)
437
    vm = models.ForeignKey(VirtualMachine)
438
    description = models.TextField()
439
    
440
    class Meta:
441
        verbose_name = u'Accounting log'
442

    
443
    def __unicode__(self):
444
        return u'%s - %s - %s - %s' % (self.user.id, self.vm.name,
445
                                       str(self.when), self.description)
446

    
447

    
448
class Disk(models.Model):
449
    name = models.CharField(max_length=255)
450
    created = models.DateTimeField('Time of creation', auto_now_add=True)
451
    updated = models.DateTimeField('Time of last update', auto_now=True)
452
    size = models.PositiveIntegerField('Disk size in GBs')
453
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
454
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
455

    
456
    class Meta:
457
        verbose_name = u'Disk instance'
458

    
459
    def __unicode__(self):
460
        return self.name
461

    
462

    
463
class Network(models.Model):
464
    NETWORK_STATES = (
465
        ('ACTIVE', 'Active'),
466
        ('DELETED', 'Deleted')
467
    )
468
    
469
    name = models.CharField(max_length=255)
470
    created = models.DateTimeField(auto_now_add=True)
471
    updated = models.DateTimeField(auto_now=True)
472
    owner = models.ForeignKey(SynnefoUser, null=True)
473
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
474
    public = models.BooleanField(default=False)
475
    link = models.ForeignKey('NetworkLink', related_name='+')
476
    machines = models.ManyToManyField(VirtualMachine,
477
                                        through='NetworkInterface')
478
    
479
    def __unicode__(self):
480
        return self.name
481

    
482

    
483
class Invitations(models.Model):
484
    source = models.ForeignKey(SynnefoUser, related_name="source")
485
    target = models.ForeignKey(SynnefoUser, related_name="target")
486
    accepted = models.BooleanField('Is the invitation accepted?', default=False)
487
    created = models.DateTimeField(auto_now_add=True)
488
    updated = models.DateTimeField(auto_now=True)
489
    level = models.IntegerField('Invitation depth level', null=True)
490

    
491
    class Meta:
492
        verbose_name = u'Invitation'
493

    
494
    def __unicode__(self):
495
        return "From: %s, To: %s" % (self.source, self.target)
496

    
497

    
498
class NetworkInterface(models.Model):
499
    FIREWALL_PROFILES = (
500
        ('ENABLED', 'Enabled'),
501
        ('DISABLED', 'Disabled'),
502
        ('PROTECTED', 'Protected')
503
    )
504
    
505
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
506
    network = models.ForeignKey(Network, related_name='nics')
507
    created = models.DateTimeField(auto_now_add=True)
508
    updated = models.DateTimeField(auto_now=True)
509
    index = models.IntegerField(null=True)
510
    mac = models.CharField(max_length=17, null=True)
511
    ipv4 = models.CharField(max_length=15, null=True)
512
    ipv6 = models.CharField(max_length=100, null=True)
513
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
514
                                        max_length=30, null=True)
515
    
516
    def __unicode__(self):
517
        return '%s@%s' % (self.machine.name, self.network.name)
518

    
519

    
520
class NetworkLink(models.Model):
521
    network = models.ForeignKey(Network, null=True, related_name='+')
522
    index = models.IntegerField()
523
    name = models.CharField(max_length=255)
524
    available = models.BooleanField(default=True)
525
    
526
    def __unicode__(self):
527
        return self.name