Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ f533f224

History | View | Annotate | Download (15.3 kB)

1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2

    
3
from django.conf import settings
4
from django.db import models
5

    
6
import datetime
7

    
8
class SynnefoUser(models.Model):
9

    
10
    #TODO: Amend this when we have groups
11
    ACCOUNT_TYPE = (
12
        ('STUDENT', 'Student'),
13
        ('PROFESSOR', 'Professor'),
14
        ('USER', 'Generic User')
15
    )
16

    
17
    name = models.CharField('Synnefo Username', max_length=255, default='')
18
    realname = models.CharField('Real Name', max_length=255, default='')
19
    uniq = models.CharField('External Unique ID', max_length=255,null=True)
20
    credit = models.IntegerField('Credit Balance')
21
    auth_token = models.CharField('Authentication Token', max_length=32, null=True)
22
    auth_token_created = models.DateTimeField('Time of auth token creation', auto_now_add=True, null = True)
23
    auth_token_expires = models.DateTimeField('Time of auth token expiration', auto_now_add=True, null = True)
24
    type = models.CharField('Current Image State', choices=ACCOUNT_TYPE, max_length=30)
25
    created = models.DateTimeField('Time of creation', auto_now_add=True)
26
    updated = models.DateTimeField('Time of last update', auto_now=True)
27

    
28
    class Meta:
29
        verbose_name = u'Synnefo User'
30

    
31
    def __unicode__(self):
32
        return self.name
33

    
34
    def get_limit(self, limit_name):
35
        """Returns the limit value for the specified limit"""
36
        limit_objs = Limit.objects.filter(name=limit_name, user=self)
37
        if len(limit_objs) == 1:
38
            return limit_objs[0].value
39
        
40
        return 0
41
        
42
    def _get_credit_quota(self):
43
        """Internal getter function for credit quota"""
44
        return self.get_limit('QUOTA_CREDIT')
45
        
46
    credit_quota = property(_get_credit_quota)
47
    
48
    def _get_monthly_rate(self):
49
        """Internal getter function for monthly credit issue rate"""
50
        return self.get_limit('MONTHLY_RATE')
51
        
52
    monthly_rate = property(_get_monthly_rate)
53
    
54
    def _get_min_credits(self):
55
        """Internal getter function for maximum number of violations"""
56
        return self.get_limit('MIN_CREDITS')
57
        
58
    min_credits = property(_get_min_credits)
59

    
60

    
61
class Image(models.Model):
62
    # This is WIP, FIXME
63
    IMAGE_STATES = (
64
        ('ACTIVE', 'Active'),
65
        ('SAVING', 'Saving'),
66
        ('DELETED', 'Deleted')
67
    )
68

    
69
    # The list of supported Image formats
70
    FORMATS = (
71
        ('dump', 'ext2 dump'),
72
        ('lvm', 'lvm snapshot')
73
    )
74

    
75
    name = models.CharField('Image name', max_length=255)
76
    state = models.CharField('Current Image State', choices=IMAGE_STATES, max_length=30)
77
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
78
    created = models.DateTimeField('Time of creation', auto_now_add=True)
79
    updated = models.DateTimeField('Time of last update', auto_now=True)
80
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
81
    backend_id = models.CharField(max_length=50, default='debian_base')
82
    format = models.CharField(choices=FORMATS, max_length=30, default='dump')
83

    
84
    class Meta:
85
        verbose_name = u'Image'
86

    
87
    def __unicode__(self):
88
        return u'%s' % ( self.name, )
89

    
90

    
91
class ImageMetadata(models.Model):
92
    meta_key = models.CharField('Image metadata key name', max_length=50)
93
    meta_value = models.CharField('Image metadata value', max_length=500)
94
    image = models.ForeignKey(Image)
95
    
96
    class Meta:
97
        verbose_name = u'Key-value pair of Image metadata.'
98
    
99
    def __unicode__(self):
100
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
101

    
102

    
103
class Limit(models.Model):
104
    LIMITS = (
105
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
106
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
107
        ('MONTHLY_RATE', 'Monthly credit issue rate')
108
    )
109
    user = models.ForeignKey(SynnefoUser)
110
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
111
    value = models.IntegerField('Limit current value')
112
    
113
    class Meta:
114
        verbose_name = u'Enforced limit for user'
115
    
116
    def __unicode__(self):
117
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
118

    
119

    
120
class Flavor(models.Model):
121
    cpu = models.IntegerField('Number of CPUs', default=0)
122
    ram = models.IntegerField('Size of RAM', default=0)             # Size in MiB
123
    disk = models.IntegerField('Size of Disk space', default=0)     # Size in GiB
124
    
125
    class Meta:
126
        verbose_name = u'Virtual machine flavor'
127
        unique_together = ("cpu","ram","disk")
128
            
129
    def _get_name(self):
130
        """Returns flavor name (generated)"""
131
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
132

    
133
    def _current_cost(self, active):
134
        """Returns active/inactive cost value
135

136
        set active = True to get active cost and False for the inactive.
137

138
        """
139
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
140
        if len(fch_list) > 0:
141
            if active:
142
                return fch_list[0].cost_active
143
            else:
144
                return fch_list[0].cost_inactive
145

    
146
        return 0
147

    
148
    def _current_cost_active(self):
149
        """Returns current active cost (property method)"""
150
        return self._current_cost(True)
151

    
152
    def _current_cost_inactive(self):
153
        """Returns current inactive cost (property method)"""
154
        return self._current_cost(False)
155

    
156
    name = property(_get_name)
157
    current_cost_active = property(_current_cost_active)
158
    current_cost_inactive = property(_current_cost_inactive)
159

    
160
    def __unicode__(self):
161
        return self.name
162

    
163

    
164
class FlavorCost(models.Model):
165
    cost_active = models.PositiveIntegerField('Active Cost')
166
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
167
    effective_from = models.DateTimeField()
168
    flavor = models.ForeignKey(Flavor)
169
    
170
    class Meta:
171
        verbose_name = u'Pricing history for flavors'
172
    
173
    def __unicode__(self):
174
        return u'Costs (up, down)=(%d, %d) for %s since %s' % \
175
                (int(self.cost_active), int(self.cost_inactive),
176
                 self.flavor.name, self.effective_from)
177

    
178

    
179
class VirtualMachine(models.Model):
180
    # The list of possible actions for a VM
181
    ACTIONS = (
182
       ('CREATE', 'Create VM'),
183
       ('START', 'Start VM'),
184
       ('STOP', 'Shutdown VM'),
185
       ('SUSPEND', 'Admin Suspend VM'),
186
       ('REBOOT', 'Reboot VM'),
187
       ('DESTROY', 'Destroy VM')
188
    )
189
    # The internal operating state of a VM
190
    OPER_STATES = (
191
        ('BUILD', 'Queued for creation'),
192
        ('ERROR', 'Creation failed'),
193
        ('STOPPED', 'Stopped'),
194
        ('STARTED', 'Started'),
195
        ('DESTROYED', 'Destroyed')
196
    )
197
    # The list of possible operations on the backend
198
    BACKEND_OPCODES = (
199
        ('OP_INSTANCE_CREATE', 'Create Instance'),
200
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
201
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
202
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
203
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
204

    
205
        # These are listed here for completeness,
206
        # and are ignored for the time being
207
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
208
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
209
    )
210
    # A backend job may be in one of the following possible states
211
    BACKEND_STATUSES = (
212
        ('queued', 'request queued'),
213
        ('waiting', 'request waiting for locks'),
214
        ('canceling', 'request being canceled'),
215
        ('running', 'request running'),
216
        ('canceled', 'request canceled'),
217
        ('success', 'request completed successfully'),
218
        ('error', 'request returned error')
219
    )
220

    
221
    # The operating state of a VM,
222
    # upon the successful completion of a backend operation.
223
    OPER_STATE_FROM_OPCODE = {
224
        'OP_INSTANCE_CREATE': 'STARTED',
225
        'OP_INSTANCE_REMOVE': 'DESTROYED',
226
        'OP_INSTANCE_STARTUP': 'STARTED',
227
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
228
        'OP_INSTANCE_REBOOT': 'STARTED',
229
        'OP_INSTANCE_SET_PARAMS': None,
230
        'OP_INSTANCE_QUERY_DATA': None
231
    }
232

    
233
    # This dictionary contains the correspondence between
234
    # internal operating states and Server States as defined
235
    # by the Rackspace API.
236
    RSAPI_STATE_FROM_OPER_STATE = {
237
        "BUILD": "BUILD",
238
        "ERROR": "ERROR",
239
        "STOPPED": "STOPPED",
240
        "STARTED": "ACTIVE",
241
        "DESTROYED": "DELETED"
242
    }
243

    
244
    name = models.CharField('Virtual Machine Name', max_length=255)
245
    owner = models.ForeignKey(SynnefoUser)
246
    created = models.DateTimeField(auto_now_add=True)
247
    updated = models.DateTimeField(auto_now=True)
248
    charged = models.DateTimeField(default=datetime.datetime.now())
249
    sourceimage = models.ForeignKey("Image", null=False) 
250
    hostid = models.CharField(max_length=100)
251
    flavor = models.ForeignKey(Flavor)
252
    deleted = models.BooleanField('Deleted', default=False)
253
    suspended = models.BooleanField('Administratively Suspended', default=False)
254

    
255
    # VM State 
256
    # The following fields are volatile data, in the sense
257
    # that they need not be persistent in the DB, but rather
258
    # get generated at runtime by quering Ganeti and applying
259
    # updates received from Ganeti.
260
    
261
    # In the future they could be moved to a separate caching layer
262
    # and removed from the database.
263
    # [vkoukis] after discussion with [faidon].
264
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
265
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
266
    backendjobid = models.PositiveIntegerField(null=True)
267
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
268
                                     null=True)
269
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30,
270
                                        null=True)
271
    backendlogmsg = models.TextField(null=True)
272

    
273
    # Error classes
274
    class InvalidBackendIdError(Exception):
275
         def __init__(self, value):
276
            self.value = value
277
         def __str__(self):
278
            return repr(self.value)
279

    
280
    class InvalidBackendMsgError(Exception):
281
         def __init__(self, opcode, status):
282
            self.opcode = opcode
283
            self.status = status
284
         def __str__(self):
285
            return repr("<opcode: %s, status: %s>" % (str(self.opcode),
286
                                                      str(self.status)))
287

    
288
    class InvalidActionError(Exception):
289
         def __init__(self, action):
290
            self._action = action
291
         def __str__(self):
292
            return repr(str(self._action))
293
    
294
    class DeletedError(Exception):
295
        pass
296
    
297
    class BuildingError(Exception):
298
        pass
299
    
300
    def __init__(self, *args, **kw):
301
        """Initialize state for just created VM instances."""
302
        super(VirtualMachine, self).__init__(*args, **kw)
303
        # This gets called BEFORE an instance gets save()d for
304
        # the first time.
305
        if not self.pk: 
306
            self.action = None
307
            self.backendjobid = None
308
            self.backendjobstatus = None
309
            self.backendopcode = None
310
            self.backendlogmsg = None
311
            self.operstate = 'BUILD'
312

    
313
    def _get_backend_id(self):
314
        """Returns the backend id for this VM by prepending backend-prefix."""
315
        if not self.id:
316
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
317
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
318

    
319
    backend_id = property(_get_backend_id)
320

    
321
    class Meta:
322
        verbose_name = u'Virtual machine instance'
323
        get_latest_by = 'created'
324
    
325
    def __unicode__(self):
326
        return self.name
327

    
328

    
329
class VirtualMachineGroup(models.Model):
330
    """Groups of VMs for SynnefoUsers"""
331
    name = models.CharField(max_length=255)
332
    created = models.DateTimeField('Time of creation', auto_now_add=True)
333
    updated = models.DateTimeField('Time of last update', auto_now=True)
334
    owner = models.ForeignKey(SynnefoUser)
335
    machines = models.ManyToManyField(VirtualMachine)
336

    
337
    class Meta:
338
        verbose_name = u'Virtual Machine Group'
339
        verbose_name_plural = 'Virtual Machine Groups'
340
        ordering = ['name']
341
    
342
    def __unicode__(self):
343
        return self.name
344

    
345

    
346
class VirtualMachineMetadata(models.Model):
347
    meta_key = models.CharField(max_length=50)
348
    meta_value = models.CharField(max_length=500)
349
    vm = models.ForeignKey(VirtualMachine)
350
    
351
    class Meta:
352
        verbose_name = u'Key-value pair of metadata for a VM.'
353
    
354
    def __unicode__(self):
355
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
356

    
357

    
358
class Debit(models.Model):
359
    when = models.DateTimeField()
360
    user = models.ForeignKey(SynnefoUser)
361
    vm = models.ForeignKey(VirtualMachine)
362
    description = models.TextField()
363
    
364
    class Meta:
365
        verbose_name = u'Accounting log'
366

    
367
    def __unicode__(self):
368
        return u'%s - %s - %s - %s' % (self.user.id, self.vm.name,
369
                                       str(self.when), self.description)
370

    
371

    
372
class Disk(models.Model):
373
    name = models.CharField(max_length=255)
374
    created = models.DateTimeField('Time of creation', auto_now_add=True)
375
    updated = models.DateTimeField('Time of last update', auto_now=True)
376
    size = models.PositiveIntegerField('Disk size in GBs')
377
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
378
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
379

    
380
    class Meta:
381
        verbose_name = u'Disk instance'
382

    
383
    def __unicode__(self):
384
        return self.name
385

    
386

    
387
class Network(models.Model):
388
    NETWORK_STATES = (
389
        ('ACTIVE', 'Active'),
390
        ('DELETED', 'Deleted')
391
    )
392
    
393
    name = models.CharField(max_length=255)
394
    created = models.DateTimeField(auto_now_add=True)
395
    updated = models.DateTimeField(auto_now=True)
396
    owner = models.ForeignKey(SynnefoUser, null=True)
397
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
398
    public = models.BooleanField(default=False)
399
    link = models.ForeignKey('NetworkLink', related_name='+')
400
    machines = models.ManyToManyField(VirtualMachine,
401
                                        through='NetworkInterface')
402
    
403
    def __unicode__(self):
404
        return self.name
405

    
406

    
407
class Invitations(models.Model):
408
    source = models.ForeignKey(SynnefoUser, related_name="source")
409
    target = models.ForeignKey(SynnefoUser, related_name="target")
410
    accepted = models.BooleanField('Is the invitation accepted?', default=False)
411
    created = models.DateTimeField(auto_now_add=True)
412
    updated = models.DateTimeField(auto_now=True)
413

    
414
    class Meta:
415
        verbose_name = u'Invitation'
416

    
417
    def __unicode__(self):
418
        return self.name
419

    
420

    
421
class NetworkInterface(models.Model):
422
    FIREWALL_PROFILES = (
423
        ('ENABLED', 'Enabled'),
424
        ('DISABLED', 'Disabled')
425
    )
426
    
427
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
428
    network = models.ForeignKey(Network, related_name='nics')
429
    created = models.DateTimeField(auto_now_add=True)
430
    updated = models.DateTimeField(auto_now=True)
431
    index = models.IntegerField(null=True)
432
    mac = models.CharField(max_length=17, null=True)
433
    ipv4 = models.CharField(max_length=15, null=True)
434
    ipv6 = models.CharField(max_length=100, null=True)
435
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
436
                                        max_length=30, null=True)
437

    
438

    
439
class NetworkLink(models.Model):
440
    network = models.ForeignKey(Network, null=True, related_name='+')
441
    index = models.IntegerField()
442
    name = models.CharField(max_length=255)
443
    available = models.BooleanField(default=True)