Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 48130e66

History | View | Annotate | Download (16.8 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
    )
45

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

    
57
    class Meta:
58
        verbose_name = u'Synnefo User'
59

    
60
    def __unicode__(self):
61
        return self.name
62

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

    
89

    
90
class Image(models.Model):
91
    # This is WIP, FIXME
92
    IMAGE_STATES = (
93
        ('ACTIVE', 'Active'),
94
        ('SAVING', 'Saving'),
95
        ('DELETED', 'Deleted')
96
    )
97

    
98
    # The list of supported Image formats
99
    FORMATS = (
100
        ('dump', 'ext2 dump'),
101
        ('lvm', 'lvm snapshot')
102
    )
103

    
104
    name = models.CharField('Image name', max_length=255)
105
    state = models.CharField('Current Image State', choices=IMAGE_STATES, max_length=30)
106
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
107
    created = models.DateTimeField('Time of creation', auto_now_add=True)
108
    updated = models.DateTimeField('Time of last update', auto_now=True)
109
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
110
    backend_id = models.CharField(max_length=50, default='debian_base')
111
    format = models.CharField(choices=FORMATS, max_length=30, default='dump')
112

    
113
    class Meta:
114
        verbose_name = u'Image'
115

    
116
    def __unicode__(self):
117
        return u'%s' % ( self.name, )
118

    
119

    
120
class ImageMetadata(models.Model):
121
    meta_key = models.CharField('Image metadata key name', max_length=50)
122
    meta_value = models.CharField('Image metadata value', max_length=500)
123
    image = models.ForeignKey(Image)
124
    
125
    class Meta:
126
        verbose_name = u'Key-value pair of Image metadata.'
127
    
128
    def __unicode__(self):
129
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
130

    
131

    
132
class Limit(models.Model):
133
    LIMITS = (
134
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
135
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
136
        ('MONTHLY_RATE', 'Monthly credit issue rate')
137
    )
138
    user = models.ForeignKey(SynnefoUser)
139
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
140
    value = models.IntegerField('Limit current value')
141
    
142
    class Meta:
143
        verbose_name = u'Enforced limit for user'
144
    
145
    def __unicode__(self):
146
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
147

    
148

    
149
class Flavor(models.Model):
150
    cpu = models.IntegerField('Number of CPUs', default=0)
151
    ram = models.IntegerField('Size of RAM', default=0)             # Size in MiB
152
    disk = models.IntegerField('Size of Disk space', default=0)     # Size in GiB
153
    
154
    class Meta:
155
        verbose_name = u'Virtual machine flavor'
156
        unique_together = ("cpu","ram","disk")
157
            
158
    def _get_name(self):
159
        """Returns flavor name (generated)"""
160
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
161

    
162
    def _current_cost(self, active):
163
        """Returns active/inactive cost value
164

165
        set active = True to get active cost and False for the inactive.
166

167
        """
168
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
169
        if len(fch_list) > 0:
170
            if active:
171
                return fch_list[0].cost_active
172
            else:
173
                return fch_list[0].cost_inactive
174

    
175
        return 0
176

    
177
    def _current_cost_active(self):
178
        """Returns current active cost (property method)"""
179
        return self._current_cost(True)
180

    
181
    def _current_cost_inactive(self):
182
        """Returns current inactive cost (property method)"""
183
        return self._current_cost(False)
184

    
185
    name = property(_get_name)
186
    current_cost_active = property(_current_cost_active)
187
    current_cost_inactive = property(_current_cost_inactive)
188

    
189
    def __unicode__(self):
190
        return self.name
191

    
192

    
193
class FlavorCost(models.Model):
194
    cost_active = models.PositiveIntegerField('Active Cost')
195
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
196
    effective_from = models.DateTimeField()
197
    flavor = models.ForeignKey(Flavor)
198
    
199
    class Meta:
200
        verbose_name = u'Pricing history for flavors'
201
    
202
    def __unicode__(self):
203
        return u'Costs (up, down)=(%d, %d) for %s since %s' % \
204
                (int(self.cost_active), int(self.cost_inactive),
205
                 self.flavor.name, self.effective_from)
206

    
207

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

    
234
        # These are listed here for completeness,
235
        # and are ignored for the time being
236
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
237
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
238
    )
239
    # A backend job may be in one of the following possible states
240
    BACKEND_STATUSES = (
241
        ('queued', 'request queued'),
242
        ('waiting', 'request waiting for locks'),
243
        ('canceling', 'request being canceled'),
244
        ('running', 'request running'),
245
        ('canceled', 'request canceled'),
246
        ('success', 'request completed successfully'),
247
        ('error', 'request returned error')
248
    )
249

    
250
    # The operating state of a VM,
251
    # upon the successful completion of a backend operation.
252
    OPER_STATE_FROM_OPCODE = {
253
        'OP_INSTANCE_CREATE': 'STARTED',
254
        'OP_INSTANCE_REMOVE': 'DESTROYED',
255
        'OP_INSTANCE_STARTUP': 'STARTED',
256
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
257
        'OP_INSTANCE_REBOOT': 'STARTED',
258
        'OP_INSTANCE_SET_PARAMS': None,
259
        'OP_INSTANCE_QUERY_DATA': None
260
    }
261

    
262
    # This dictionary contains the correspondence between
263
    # internal operating states and Server States as defined
264
    # by the Rackspace API.
265
    RSAPI_STATE_FROM_OPER_STATE = {
266
        "BUILD": "BUILD",
267
        "ERROR": "ERROR",
268
        "STOPPED": "STOPPED",
269
        "STARTED": "ACTIVE",
270
        "DESTROYED": "DELETED"
271
    }
272

    
273
    name = models.CharField('Virtual Machine Name', max_length=255)
274
    owner = models.ForeignKey(SynnefoUser)
275
    created = models.DateTimeField(auto_now_add=True)
276
    updated = models.DateTimeField(auto_now=True)
277
    charged = models.DateTimeField(default=datetime.datetime.now())
278
    sourceimage = models.ForeignKey("Image", null=False) 
279
    hostid = models.CharField(max_length=100)
280
    flavor = models.ForeignKey(Flavor)
281
    deleted = models.BooleanField('Deleted', default=False)
282
    suspended = models.BooleanField('Administratively Suspended', default=False)
283

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

    
302
    # Error classes
303
    class InvalidBackendIdError(Exception):
304
         def __init__(self, value):
305
            self.value = value
306
         def __str__(self):
307
            return repr(self.value)
308

    
309
    class InvalidBackendMsgError(Exception):
310
         def __init__(self, opcode, status):
311
            self.opcode = opcode
312
            self.status = status
313
         def __str__(self):
314
            return repr("<opcode: %s, status: %s>" % (str(self.opcode),
315
                                                      str(self.status)))
316

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

    
342
    def _get_backend_id(self):
343
        """Returns the backend id for this VM by prepending backend-prefix."""
344
        if not self.id:
345
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
346
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
347

    
348
    backend_id = property(_get_backend_id)
349

    
350
    class Meta:
351
        verbose_name = u'Virtual machine instance'
352
        get_latest_by = 'created'
353
    
354
    def __unicode__(self):
355
        return self.name
356

    
357

    
358
class VirtualMachineGroup(models.Model):
359
    """Groups of VMs for SynnefoUsers"""
360
    name = models.CharField(max_length=255)
361
    created = models.DateTimeField('Time of creation', auto_now_add=True)
362
    updated = models.DateTimeField('Time of last update', auto_now=True)
363
    owner = models.ForeignKey(SynnefoUser)
364
    machines = models.ManyToManyField(VirtualMachine)
365

    
366
    class Meta:
367
        verbose_name = u'Virtual Machine Group'
368
        verbose_name_plural = 'Virtual Machine Groups'
369
        ordering = ['name']
370
    
371
    def __unicode__(self):
372
        return self.name
373

    
374

    
375
class VirtualMachineMetadata(models.Model):
376
    meta_key = models.CharField(max_length=50)
377
    meta_value = models.CharField(max_length=500)
378
    vm = models.ForeignKey(VirtualMachine)
379
    
380
    class Meta:
381
        verbose_name = u'Key-value pair of metadata for a VM.'
382
    
383
    def __unicode__(self):
384
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
385

    
386

    
387
class Debit(models.Model):
388
    when = models.DateTimeField()
389
    user = models.ForeignKey(SynnefoUser)
390
    vm = models.ForeignKey(VirtualMachine)
391
    description = models.TextField()
392
    
393
    class Meta:
394
        verbose_name = u'Accounting log'
395

    
396
    def __unicode__(self):
397
        return u'%s - %s - %s - %s' % (self.user.id, self.vm.name,
398
                                       str(self.when), self.description)
399

    
400

    
401
class Disk(models.Model):
402
    name = models.CharField(max_length=255)
403
    created = models.DateTimeField('Time of creation', auto_now_add=True)
404
    updated = models.DateTimeField('Time of last update', auto_now=True)
405
    size = models.PositiveIntegerField('Disk size in GBs')
406
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
407
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
408

    
409
    class Meta:
410
        verbose_name = u'Disk instance'
411

    
412
    def __unicode__(self):
413
        return self.name
414

    
415

    
416
class Network(models.Model):
417
    NETWORK_STATES = (
418
        ('ACTIVE', 'Active'),
419
        ('DELETED', 'Deleted')
420
    )
421
    
422
    name = models.CharField(max_length=255)
423
    created = models.DateTimeField(auto_now_add=True)
424
    updated = models.DateTimeField(auto_now=True)
425
    owner = models.ForeignKey(SynnefoUser, null=True)
426
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
427
    public = models.BooleanField(default=False)
428
    link = models.ForeignKey('NetworkLink', related_name='+')
429
    machines = models.ManyToManyField(VirtualMachine,
430
                                        through='NetworkInterface')
431
    
432
    def __unicode__(self):
433
        return self.name
434

    
435

    
436
class Invitations(models.Model):
437
    source = models.ForeignKey(SynnefoUser, related_name="source")
438
    target = models.ForeignKey(SynnefoUser, related_name="target")
439
    accepted = models.BooleanField('Is the invitation accepted?', default=False)
440
    created = models.DateTimeField(auto_now_add=True)
441
    updated = models.DateTimeField(auto_now=True)
442

    
443
    class Meta:
444
        verbose_name = u'Invitation'
445

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

    
449

    
450
class NetworkInterface(models.Model):
451
    FIREWALL_PROFILES = (
452
        ('ENABLED', 'Enabled'),
453
        ('DISABLED', 'Disabled')
454
    )
455
    
456
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
457
    network = models.ForeignKey(Network, related_name='nics')
458
    created = models.DateTimeField(auto_now_add=True)
459
    updated = models.DateTimeField(auto_now=True)
460
    index = models.IntegerField(null=True)
461
    mac = models.CharField(max_length=17, null=True)
462
    ipv4 = models.CharField(max_length=15, null=True)
463
    ipv6 = models.CharField(max_length=100, null=True)
464
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
465
                                        max_length=30, null=True)
466

    
467

    
468
class NetworkLink(models.Model):
469
    network = models.ForeignKey(Network, null=True, related_name='+')
470
    index = models.IntegerField()
471
    name = models.CharField(max_length=255)
472
    available = models.BooleanField(default=True)