Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 21d8adbf

History | View | Annotate | Download (18.7 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
    name = models.CharField('Synnefo Username', max_length=255, default='')
49
    realname = models.CharField('Real Name', max_length=255, default='')
50
    uniq = models.CharField('External Unique ID', max_length=255,null=True)
51
    credit = models.IntegerField('Credit Balance')
52
    auth_token = models.CharField('Authentication Token', max_length=32, null=True)
53
    auth_token_created = models.DateTimeField('Time of auth token creation', auto_now_add=True, null = True)
54
    auth_token_expires = models.DateTimeField('Time of auth token expiration', auto_now_add=True, null = True)
55
    tmp_auth_token = models.CharField('Temporary authentication token', max_length=32, null=True)
56
    tmp_auth_token_expires = models.DateTimeField('Time of temporary auth token expiration', auto_now_add=True, null = True)
57
    type = models.CharField('Account type', choices=ACCOUNT_TYPE,
58
                            max_length=30)
59
    created = models.DateTimeField('Time of creation', auto_now_add=True)
60
    updated = models.DateTimeField('Time of last update', auto_now=True)
61
    max_invitations = models.IntegerField('Max number of invitations',
62
                                          null=True)
63

    
64
    class Meta:
65
        verbose_name = u'Synnefo User'
66

    
67
    def __unicode__(self):
68
        return self.name
69

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

    
96

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

    
105
    # The list of supported Image formats
106
    FORMATS = (
107
        ('dump', 'ext3 dump'),
108
        ('extdump', 'Raw ext2/3/4 dump'),
109
        ('lvm', 'lvm snapshot'),
110
        ('ntfsclone', 'Windows Image produced by ntfsclone'),
111
        ('ntfsdump', 'Raw NTFS dump')
112
    )
113

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

    
125
    class Meta:
126
        verbose_name = u'Image'
127

    
128
    def __unicode__(self):
129
        return u'%s' % ( self.name, )
130

    
131

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

    
143

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

    
160

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

    
174
    def _current_cost(self, active):
175
        """Returns active/inactive cost value
176

177
        set active = True to get active cost and False for the inactive.
178

179
        """
180
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
181
        if len(fch_list) > 0:
182
            if active:
183
                return fch_list[0].cost_active
184
            else:
185
                return fch_list[0].cost_inactive
186

    
187
        return 0
188

    
189
    def _current_cost_active(self):
190
        """Returns current active cost (property method)"""
191
        return self._current_cost(True)
192

    
193
    def _current_cost_inactive(self):
194
        """Returns current inactive cost (property method)"""
195
        return self._current_cost(False)
196

    
197
    name = property(_get_name)
198
    current_cost_active = property(_current_cost_active)
199
    current_cost_inactive = property(_current_cost_inactive)
200

    
201
    def __unicode__(self):
202
        return self.name
203

    
204

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

    
219

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

    
246
        # These are listed here for completeness,
247
        # and are ignored for the time being
248
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
249
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
250
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
251
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
252
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
253
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
254
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
255
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
256
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks')
257
    )
258
    # A backend job may be in one of the following possible states
259
    BACKEND_STATUSES = (
260
        ('queued', 'request queued'),
261
        ('waiting', 'request waiting for locks'),
262
        ('canceling', 'request being canceled'),
263
        ('running', 'request running'),
264
        ('canceled', 'request canceled'),
265
        ('success', 'request completed successfully'),
266
        ('error', 'request returned error')
267
    )
268

    
269
    # The operating state of a VM,
270
    # upon the successful completion of a backend operation.
271
    # IMPORTANT: Make sure all keys have a corresponding
272
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
273
    OPER_STATE_FROM_OPCODE = {
274
        'OP_INSTANCE_CREATE': 'STARTED',
275
        'OP_INSTANCE_REMOVE': 'DESTROYED',
276
        'OP_INSTANCE_STARTUP': 'STARTED',
277
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
278
        'OP_INSTANCE_REBOOT': 'STARTED',
279
        'OP_INSTANCE_SET_PARAMS': None,
280
        'OP_INSTANCE_QUERY_DATA': None,
281
        'OP_INSTANCE_REINSTALL' : None,
282
        'OP_INSTANCE_ACTIVATE_DISKS' : None,
283
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
284
        'OP_INSTANCE_REPLACE_DISKS' : None,
285
        'OP_INSTANCE_MIGRATE': None,
286
        'OP_INSTANCE_CONSOLE': None,
287
        'OP_INSTANCE_RECREATE_DISKS': None
288
    }
289

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

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

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

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

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

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

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

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

    
380
    backend_id = property(_get_backend_id)
381

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

    
389

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

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

    
406

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

    
418

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

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

    
432

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

    
441
    class Meta:
442
        verbose_name = u'Disk instance'
443

    
444
    def __unicode__(self):
445
        return self.name
446

    
447

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

    
467

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

    
476
    class Meta:
477
        verbose_name = u'Invitation'
478

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

    
482

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

    
504

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