Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ ace4bd5d

History | View | Annotate | Download (17.1 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,
106
                                max_length=30)
107
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
108
    created = models.DateTimeField('Time of creation', auto_now_add=True)
109
    updated = models.DateTimeField('Time of last update', auto_now=True)
110
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
111
    backend_id = models.CharField(max_length=50, default='debian_base')
112
    format = models.CharField(choices=FORMATS, max_length=30, default='dump')
113
    public = models.BooleanField(default=False)
114

    
115
    class Meta:
116
        verbose_name = u'Image'
117

    
118
    def __unicode__(self):
119
        return u'%s' % ( self.name, )
120

    
121

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

    
133

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

    
150

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

    
164
    def _current_cost(self, active):
165
        """Returns active/inactive cost value
166

167
        set active = True to get active cost and False for the inactive.
168

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

    
177
        return 0
178

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

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

    
187
    name = property(_get_name)
188
    current_cost_active = property(_current_cost_active)
189
    current_cost_inactive = property(_current_cost_inactive)
190

    
191
    def __unicode__(self):
192
        return self.name
193

    
194

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

    
209

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

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

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

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

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

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

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

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

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

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

    
350
    backend_id = property(_get_backend_id)
351

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

    
359

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

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

    
376

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

    
388

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

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

    
402

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

    
411
    class Meta:
412
        verbose_name = u'Disk instance'
413

    
414
    def __unicode__(self):
415
        return self.name
416

    
417

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

    
437

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

    
445
    class Meta:
446
        verbose_name = u'Invitation'
447

    
448
    def __unicode__(self):
449
        return self.name
450

    
451

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

    
473

    
474
class NetworkLink(models.Model):
475
    network = models.ForeignKey(Network, null=True, related_name='+')
476
    index = models.IntegerField()
477
    name = models.CharField(max_length=255)
478
    available = models.BooleanField(default=True)
479
    
480
    def __unicode__(self):
481
        return self.name