Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 52194743

History | View | Annotate | Download (15.8 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
from django.contrib.auth.models import User
6
from django.utils.translation import gettext_lazy as _
7

    
8
import datetime
9

    
10
backend_prefix_id = settings.BACKEND_PREFIX_ID
11

    
12
class SynnefoUser(models.Model):
13
    name = models.CharField(max_length=255)
14
    credit = models.IntegerField()
15
    created = models.DateTimeField('Time of creation', auto_now_add=True)
16
    updated = models.DateTimeField('Time of last update', auto_now=True)
17
    monthly_rate = models.IntegerField()
18
    user = models.ForeignKey(User)
19
    violations = models.IntegerField()
20
    
21
    class Meta:
22
        verbose_name = u'Synnefo User'
23
    
24
    def __unicode__(self):
25
        return self.name
26
    
27
    def charge_credits(self, cost, start, end):
28
        """Reduce user credits for specified duration.
29
        
30
        Returns amount of credits remaining. Negative if the user has surpassed his limit.
31
        
32
        """
33
        td = end - start
34
        sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
35
        
36
        total_hours = float(sec) / float(60.0*60.0)
37
        total_cost = float(cost)*total_hours
38
        
39
        self.credit = self.credit - round(total_cost)
40
        
41
        if self.credit < 0:
42
            self.violations = self.violations + 1
43
        else:
44
            self.violations = 0
45
                
46
        return self.credit
47
    
48
    def allocate_credits(self):
49
        """Allocate credits. Add monthly rate to user credit reserve."""
50
        self.credit = self.credit + self.monthly_rate
51
        
52
        # ensure that the user has not more credits than his quota
53
        limit_quota = self.get_limit('QUOTA_CREDIT')
54
                
55
        if self.credit > limit_quota:
56
            self.credit = limit_quota
57

    
58
    def get_limit(self, limit_name):
59
        """Returns the limit value for the specified limit"""
60
        limit_objs = Limit.objects.filter(name=limit_name, user=self)
61
        
62
        if len(limit_objs) == 1:
63
            return limit_objs[0].value
64
        
65
        return 0
66
        
67

    
68
class Image(models.Model):
69
    # This is WIP, FIXME
70
    IMAGE_STATES = (
71
                ('ACTIVE', 'Active'),
72
                ('SAVING', 'Saving'),
73
                ('DELETED', 'Deleted')
74
    )
75

    
76
    name = models.CharField(max_length=255, help_text=_('description'))
77
    state = models.CharField(choices=IMAGE_STATES, max_length=30)
78
    description = models.TextField(help_text=_('description'))
79
    owner = models.ForeignKey(SynnefoUser,blank=True, null=True)
80
    created = models.DateTimeField('Time of creation', auto_now_add=True)
81
    updated = models.DateTimeField('Time of last update', auto_now=True)
82

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

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

    
89
    def get_vmid(self):
90
        """Returns first Virtual Machine's id, if any
91
           an Image might be the ForeignKey to one or many VirtualMachines
92
           we want the vm that created the Image (the first one)
93
        """
94
        if self.virtualmachine_set.all():
95
            return self.virtualmachine_set.all()[0].id
96
        else:
97
            return ''
98

    
99
    vm_id = property(get_vmid)
100

    
101

    
102
class ImageMetadata(models.Model):
103
    meta_key = models.CharField(max_length=50)
104
    meta_value = models.CharField(max_length=500)
105
    image = models.ForeignKey(Image)
106
    
107
    class Meta:
108
        verbose_name = u'Key-value pair of metadata for an Image.'
109
    
110
    def __unicode__(self):
111
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
112

    
113

    
114
class Limit(models.Model):
115
    LIMITS = (
116
        ('QUOTA_CREDIT', 'Maximum number of credits per user'),
117
        ('MAX_VIOLATIONS', 'Maximum number of credit violation per user')
118
    )
119
    user = models.ForeignKey(SynnefoUser)
120
    name = models.CharField(choices=LIMITS, max_length=30, null=False)
121
    value = models.IntegerField()
122
    
123
    class Meta:
124
        verbose_name = u'Enforced limit for user'
125
    
126
    def __unicode__(self):
127
        return u'Limit %s for user %s: %d' % (self.limit, self.user, self.value)
128

    
129

    
130
class Flavor(models.Model):
131
    cpu = models.IntegerField(default=0)
132
    ram = models.IntegerField(default=0)
133
    disk = models.IntegerField(default=0)
134
    
135
    class Meta:
136
        verbose_name = u'Virtual machine flavor'
137
            
138
    def _get_name(self):
139
        """Returns flavor name (generated)"""
140
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
141

    
142
    def _get_cost_inactive(self):
143
        """Returns the inactive cost for a Flavor (usually only disk usage counts)"""
144
        self._update_costs()
145
        return self._cost_inactive
146

    
147
    def _get_cost_active(self):
148
        """Returns the active cost for a Flavor"""
149
        self._update_costs()
150
        return self._cost_active
151
    
152
    def _update_costs(self):
153
        """Update the internal cost_active, cost_inactive variables"""
154
        if not hasattr(self, '_cost_active'):
155
            fch_list = FlavorCostHistory.objects.filter(flavor=self).order_by('-effective_from')
156
            if len(fch_list) > 0:
157
                fch = fch_list[0]
158
                self._cost_active = fch.cost_active
159
                self._cost_inactive = fch.cost_inactive
160
            else:
161
                self._cost_active = 0
162
                self._cost_inactive = 0
163

    
164
    name = property(_get_name)
165
    cost_active = property(_get_cost_active)
166
    cost_inactive = property(_get_cost_inactive)
167

    
168
    def __unicode__(self):
169
        return self.name
170
    
171
    def get_price_list(self):
172
        """Returns the price catalog for this Flavor"""
173
        fch_list = FlavorCostHistory.objects.filter(flavor=self).order_by('effective_from')
174
        
175
        return fch_list
176
        
177
    def find_cost(self, the_date):
178
        """Returns costs (FlavorCostHistory instance) for the specified date (the_date)"""
179
        rdate = None
180
        fch_list = self.get_price_list()
181
        
182
        for fc in fch_list:
183
            if the_date > fc.effective_from:
184
                rdate = fc
185
            else:
186
                break
187
        
188
        return rdate
189

    
190

    
191
class FlavorCostHistory(models.Model):
192
    cost_active = models.PositiveIntegerField()
193
    cost_inactive = models.PositiveIntegerField()
194
    effective_from = models.DateField()
195
    flavor = models.ForeignKey(Flavor)
196
    
197
    class Meta:
198
        verbose_name = u'Pricing history for flavors'
199
    
200
    def __unicode__(self):
201
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
202

    
203

    
204
class VirtualMachine(models.Model):
205
    ACTIONS = (
206
       ('CREATE', 'Create VM'),
207
       ('START', 'Start VM'),
208
       ('STOP', 'Shutdown VM'),
209
       ('SUSPEND', 'Admin Suspend VM'),
210
       ('REBOOT', 'Reboot VM'),
211
       ('DESTROY', 'Destroy VM')
212
    )
213

    
214
    OPER_STATES = (
215
        ('BUILD', 'Queued for creation'),
216
        ('ERROR', 'Creation failed'),
217
        ('STOPPED', 'Stopped'),
218
        ('STARTED', 'Started'),
219
        ('DESTROYED', 'Destroyed')
220
    )
221

    
222
    BACKEND_OPCODES = (
223
        ('OP_INSTANCE_CREATE', 'Create Instance'),
224
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
225
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
226
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
227
        ('OP_INSTANCE_REBOOT', 'Reboot Instance')
228
    )
229

    
230
    BACKEND_STATUSES = (
231
        ('queued', 'request queued'),
232
        ('waiting', 'request waiting for locks'),
233
        ('canceling', 'request being canceled'),
234
        ('running', 'request running'),
235
        ('canceled', 'request canceled'),
236
        ('success', 'request completed successfully'),
237
        ('error', 'request returned error')
238
    )
239

    
240
    # The operating state of a VM,
241
    # upon the successful completion of a backend operation.
242
    OPER_STATE_FROM_OPCODE = {
243
        'OP_INSTANCE_CREATE': 'STARTED',
244
        'OP_INSTANCE_REMOVE': 'DESTROYED',
245
        'OP_INSTANCE_STARTUP': 'STARTED',
246
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
247
        'OP_INSTANCE_REBOOT': 'STARTED'
248
    }
249

    
250
    RSAPI_STATE_FROM_OPER_STATE = {
251
        "BUILD": "BUILD",
252
        "ERROR": "ERROR",
253
        "STOPPED": "STOPPED",
254
        "STARTED": "ACTIVE",
255
        "DESTROYED": "DELETED"
256
    }
257

    
258
    name = models.CharField(max_length=255)
259
    owner = models.ForeignKey(SynnefoUser,blank=True, null=True)
260
    created = models.DateTimeField('Time of creation', auto_now_add=True)
261
    updated = models.DateTimeField('Time of last update', auto_now=True)
262
    charged = models.DateTimeField('Time of last charge', default=datetime.datetime.now())
263
    # Use string reference to avoid circular ForeignKey def.
264
    # FIXME: "sourceimage" works, "image" causes validation errors. See "related_name" in the Django docs.
265
    sourceimage = models.ForeignKey(Image, null=False) 
266
    hostid = models.CharField(max_length=100)
267
    description = models.TextField(help_text=_('description'))
268
    ipfour = models.IPAddressField()
269
    ipsix = models.CharField(max_length=100)
270
    flavor = models.ForeignKey(Flavor)
271
    suspended = models.BooleanField('Administratively Suspended')
272

    
273
    # VM State 
274
    # The following fields are volatile data, in the sense
275
    # that they need not be persistent in the DB, but rather
276
    # get generated at runtime by quering Ganeti and applying
277
    # updates received from Ganeti.
278
    #
279
    # They belong to a separate caching layer, in the long run.
280
    # [vkoukis] after discussion with [faidon].
281
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
282
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
283
    _backendjobid = models.PositiveIntegerField(null=True)
284
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
285
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
286
    _backendlogmsg = models.TextField(null=True)
287

    
288
    # Error classes
289
    class InvalidBackendIdError(Exception):
290
         def __init__(self, value):
291
            self.value = value
292
         def __str__(self):
293
            return repr(self.value)
294

    
295
    class InvalidBackendMsgError(Exception):
296
         def __init__(self, opcode, status):
297
            self.opcode = opcode
298
            self.status = status
299
         def __str__(self):
300
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
301

    
302
    class InvalidActionError(Exception):
303
         def __init__(self, action):
304
            self.__action = action
305
         def __str__(self):
306
            return repr(str(self._action))
307

    
308
    @staticmethod
309
    def id_from_instance_name(name):
310
        """Returns VirtualMachine's Django id, given a ganeti machine name.
311

312
        Strips the ganeti prefix atm. Needs a better name!
313
        
314
        """
315
        if not str(name).startswith(backend_prefix_id):
316
            raise VirtualMachine.InvalidBackendIdError(str(name))
317
        ns = str(name).lstrip(backend_prefix_id)
318
        if not ns.isdigit():
319
            raise VirtualMachine.InvalidBackendIdError(str(name))
320
        return int(ns)
321

    
322
    def __init__(self, *args, **kw):
323
        """Initialize state for just created VM instances."""
324
        super(VirtualMachine, self).__init__(*args, **kw)
325
        # Before this instance gets save()d
326
        if not self.pk: 
327
            self._action = None
328
            self._operstate = "BUILD"
329
            self._backendjobid = None
330
            self._backendjobstatus = None
331
            self._backendopcode = None
332
            self._backendlogmsg = None
333

    
334
    def process_backend_msg(self, jobid, opcode, status, logmsg):
335
        """Process a job progress notification from the backend.
336

337
        Process an incoming message from the backend (currently Ganeti).
338
        Job notifications with a terminating status (sucess, error, or canceled),
339
        also update the operating state of the VM.
340

341
        """
342
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
343
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
344
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
345

    
346
        self._backendjobid = jobid
347
        self._backendjobstatus = status
348
        self._backendopcode = opcode
349
        self._backendlogmsg = logmsg
350

    
351
        # Notifications of success change the operating state
352
        if status == 'success':
353
            self._operstate = VirtualMachine.OPER_STATE_FROM_OPCODE[opcode]
354
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
355
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
356
            self._operstate = 'ERROR'
357
        # Any other notification of failure leaves the operating state unchanged
358

    
359
        self.save()
360

    
361
    def start_action(self, action):
362
        """Update the state of a VM when a new action is initiated."""
363
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
364
            raise VirtualMachine.InvalidActionError(action)
365

    
366
        self._action = action
367
        self._backendjobid = None
368
        self._backendopcode = None
369
        self._backendlogmsg = None
370
        
371
        self.save()
372

    
373
    # FIXME: Perhaps move somewhere else, outside the model?
374
    def _get_rsapi_state(self):
375
        try:
376
            return VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
377
        except KeyError:
378
            return "UNKNOWN"
379

    
380
    rsapi_state = property(_get_rsapi_state)
381

    
382
    def _get_backend_id(self):
383
        """Returns the backend id for this VM by prepending backend-prefix."""
384
        return '%s%s' % (backend_prefix_id, str(self.id))
385

    
386
    backend_id = property(_get_backend_id)
387

    
388
    class Meta:
389
        verbose_name = u'Virtual machine instance'
390
        get_latest_by = 'created'
391
    
392
    def __unicode__(self):
393
        return self.name
394

    
395
    def get_accounting_logs(self):
396
        """Returns all AcountingLog records after the charged field"""
397
        acc_logs = AccountingLog.objects.filter(date__gte=self.charged, vm=self)
398
        if len(acc_logs) == 0:
399
            return []
400
            
401
        return acc_logs
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)
425
    
426
    class Meta:
427
        verbose_name = u'Key-value pair of metadata for a VM.'
428
    
429
    def __unicode__(self):
430
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
431

    
432

    
433
class AccountingLog(models.Model):
434
    vm = models.ForeignKey(VirtualMachine)
435
    date = models.DateTimeField()
436
    state = models.CharField(choices=VirtualMachine.OPER_STATES, max_length=30)
437
    
438
    class Meta:
439
        verbose_name = u'Accounting log'
440

    
441
    def __unicode__(self):
442
        return u'%s - %s - %s' % (self.vm.name, str(self.date), self.state)
443
    
444
    @staticmethod   
445
    def get_log_entries(vm_obj, date_from):
446
        """Returns log entries for the specified vm after a date"""
447
        entries = AccountingLog.objects.filter(vm=vm_obj).filter(date__gte=date_from).order_by('-date')
448
    
449
        return entries
450

    
451

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

    
460
    class Meta:
461
        verbose_name = u'Disk instance'
462

    
463
    def __unicode__(self):
464
        return self.name