Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ eae0a59a

History | View | Annotate | Download (19.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

    
7
import datetime
8

    
9
backend_prefix_id = settings.BACKEND_PREFIX_ID
10

    
11
class SynnefoUser(models.Model):
12
    name = models.CharField('Synnefo Username', max_length=255)
13
    credit = models.IntegerField('Credit Balance')
14
    created = models.DateTimeField('Time of creation', auto_now_add=True)
15
    updated = models.DateTimeField('Time of last update', auto_now=True)
16
    #
17
    # We do not rely on Django's user authentication mechanism.
18
    # Hence, no references to the User model.
19
    # [vkoukis], after discussion with [faidon].
20
    # user = models.ForeignKey(User)
21
    
22
    class Meta:
23
        verbose_name = u'Synnefo User'
24
    
25
    def __unicode__(self):
26
        return self.name 
27

    
28
    def get_limit(self, limit_name):
29
        """Returns the limit value for the specified limit"""
30
        limit_objs = Limit.objects.filter(name=limit_name, user=self)        
31
        if len(limit_objs) == 1:
32
            return limit_objs[0].value
33
        
34
        return 0
35
        
36
    def _get_credit_quota(self):
37
        """Internal getter function for credit quota"""
38
        return self.get_limit('QUOTA_CREDIT')
39
        
40
    credit_quota = property(_get_credit_quota)
41
    
42
    def _get_monthly_rate(self):
43
        """Internal getter function for monthly credit issue rate"""
44
        return self.get_limit('MONTHLY_RATE')
45
        
46
    monthly_rate = property(_get_monthly_rate)
47
    
48
    def _get_min_credits(self):
49
        """Internal getter function for maximum number of violations"""
50
        return self.get_limit('MIN_CREDITS')
51
        
52
    min_credits = property(_get_min_credits)
53
    
54
    def debit_account(self, amount, vm, description):
55
        """Charges the user with the specified amount of credits for a vm (resource)"""
56
        date_now = datetime.datetime.now()
57

    
58
        # FIXME: The following two actions (debiting the user
59
        # and creating the Debit instance must happen ATOMICALLY,
60
        # as specified in the documentation.
61

    
62
        self.credit = self.credit - amount
63
        self.save()
64

    
65
        # then write the debit entry
66
        debit = Debit()
67

    
68
        debit.user = self
69
        debit.vm = vm
70
        debit.when = date_now
71
        debit.description = description
72

    
73
        debit.save()
74

    
75
    def credit_account(self, amount, creditor, description):
76
        """No clue :)"""
77
        return
78

    
79

    
80
class Image(models.Model):
81
    # This is WIP, FIXME
82
    IMAGE_STATES = (
83
        ('ACTIVE', 'Active'),
84
        ('SAVING', 'Saving'),
85
        ('DELETED', 'Deleted')
86
    )
87

    
88
    name = models.CharField('Image name', max_length=255)
89
    state = models.CharField('Current Image State', choices=IMAGE_STATES, max_length=30)
90
    description = models.TextField('General description')
91
    size = models.PositiveIntegerField('Image size in MBs')
92
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
93
    created = models.DateTimeField('Time of creation', auto_now_add=True)
94
    updated = models.DateTimeField('Time of last update', auto_now=True)
95
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
96

    
97
    class Meta:
98
        verbose_name = u'Image'
99

    
100
    def __unicode__(self):
101
        return u'%s' % ( self.name, )
102

    
103

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

    
115

    
116
class Limit(models.Model):
117
    LIMITS = (
118
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
119
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
120
        ('MONTHLY_RATE', 'Monthly credit issue rate')
121
    )
122
    user = models.ForeignKey(SynnefoUser)
123
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
124
    value = models.IntegerField('Limit current value')
125
    
126
    class Meta:
127
        verbose_name = u'Enforced limit for user'
128
    
129
    def __unicode__(self):
130
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
131

    
132

    
133
class Flavor(models.Model):
134
    cpu = models.IntegerField('Number of CPUs', default=0)
135
    ram = models.IntegerField('Size of RAM', default=0)
136
    disk = models.IntegerField('Size of Disk space', default=0)
137
    
138
    class Meta:
139
        verbose_name = u'Virtual machine flavor'
140
        unique_together = ("cpu","ram","disk")
141
            
142
    def _get_name(self):
143
        """Returns flavor name (generated)"""
144
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
145

    
146
    def _current_cost(self, active):
147
        """Returns active/inactive cost value
148

149
        set active = True to get active cost and False for the inactive.
150

151
        """
152
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
153
        if len(fch_list) > 0:
154
            if active:
155
                return fch_list[0].cost_active
156
            else:
157
                return fch_list[0].cost_inactive
158

    
159
        return 0
160
        
161

    
162
    def _current_cost_active(self):
163
        """Returns current active cost (property method)"""
164
        return self._current_cost(True)
165

    
166
    def _current_cost_inactive(self):
167
        """Returns current inactive cost (property method)"""
168
        return self._current_cost(False)
169

    
170
    name = property(_get_name)
171
    current_cost_active = property(_current_cost_active)
172
    current_cost_inactive = property(_current_cost_inactive)
173

    
174
    def __unicode__(self):
175
        return self.name
176

    
177
    def _get_costs(self, start_datetime, end_datetime, active):
178
        """Return a list with FlavorCost objects for the specified duration"""
179
        def between(enh_fc, a_date):
180
            """Checks if a date is between a FlavorCost duration"""
181
            if enh_fc.effective_from <= a_date and enh_fc.effective_to is None:
182
                return True
183

    
184
            return enh_fc.effective_from <= a_date and enh_fc.effective_to >= a_date
185

    
186
        def calculate_cost(start_date, end_date, cost):
187
            """Calculate the total cost for the specified duration"""
188
            td = end_date - start_date
189
            sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
190
            total_hours = float(sec) / float(60.0*60.0)
191
            total_cost = float(cost)*total_hours
192

    
193
            return round(total_cost)
194

    
195
        # Get the related FlavorCost objects, sorted.
196
        price_list = FlavorCost.objects.filter(flavor=self).order_by('effective_from')
197

    
198
        # add the extra field FlavorCost.effective_to
199
        for idx in range(0, len(price_list)):
200
            if idx + 1 == len(price_list):
201
                price_list[idx].effective_to = None
202
            else:
203
                price_list[idx].effective_to = price_list[idx + 1].effective_from
204

    
205
        price_result = []
206
        found_start = False
207

    
208
        # Find the affected FlavorCost, according to the
209
        # dates, and put them in price_result
210
        for p in price_list:
211
            if between(p, start_datetime):
212
                found_start = True
213
                p.effective_from = start_datetime
214
            if between(p, end_datetime):
215
                p.effective_to = end_datetime
216
                price_result.append(p)
217
                break
218
            if found_start:
219
                price_result.append(p)
220

    
221
        results = []
222

    
223
        # Create the list and the result tuples
224
        for p in price_result:
225
            if active:
226
                cost = p.cost_active
227
            else:
228
                cost = p.cost_inactive
229

    
230
            results.append( ( str(p.effective_from), calculate_cost(p.effective_from, p.effective_to, cost)) )
231

    
232
        return results
233

    
234
    def get_cost_active(self, start_datetime, end_datetime):
235
        """Returns a list with the active costs for the specified duration"""
236
        return self._get_costs(start_datetime, end_datetime, True)
237

    
238
    def get_cost_inactive(self, start_datetime, end_datetime):
239
        """Returns a list with the inactive costs for the specified duration"""
240
        return self._get_costs(start_datetime, end_datetime, False)
241

    
242

    
243
class FlavorCost(models.Model):
244
    cost_active = models.PositiveIntegerField('Active Cost')
245
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
246
    effective_from = models.DateTimeField()
247
    flavor = models.ForeignKey(Flavor)
248
    
249
    class Meta:
250
        verbose_name = u'Pricing history for flavors'
251
    
252
    def __unicode__(self):
253
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
254

    
255

    
256
class VirtualMachine(models.Model):
257
    # The list of possible actions for a VM
258
    ACTIONS = (
259
       ('CREATE', 'Create VM'),
260
       ('START', 'Start VM'),
261
       ('STOP', 'Shutdown VM'),
262
       ('SUSPEND', 'Admin Suspend VM'),
263
       ('REBOOT', 'Reboot VM'),
264
       ('DESTROY', 'Destroy VM')
265
    )
266
    # The internal operating state of a VM
267
    OPER_STATES = (
268
        ('BUILD', 'Queued for creation'),
269
        ('ERROR', 'Creation failed'),
270
        ('STOPPED', 'Stopped'),
271
        ('STARTED', 'Started'),
272
        ('DESTROYED', 'Destroyed')
273
    )
274
    # The list of possible operations on the backend
275
    BACKEND_OPCODES = (
276
        ('OP_INSTANCE_CREATE', 'Create Instance'),
277
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
278
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
279
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
280
        ('OP_INSTANCE_REBOOT', 'Reboot Instance')
281
    )
282
    # A backend job may be in one of the following possible states
283
    BACKEND_STATUSES = (
284
        ('queued', 'request queued'),
285
        ('waiting', 'request waiting for locks'),
286
        ('canceling', 'request being canceled'),
287
        ('running', 'request running'),
288
        ('canceled', 'request canceled'),
289
        ('success', 'request completed successfully'),
290
        ('error', 'request returned error')
291
    )
292

    
293
    # The operating state of a VM,
294
    # upon the successful completion of a backend operation.
295
    OPER_STATE_FROM_OPCODE = {
296
        'OP_INSTANCE_CREATE': 'STARTED',
297
        'OP_INSTANCE_REMOVE': 'DESTROYED',
298
        'OP_INSTANCE_STARTUP': 'STARTED',
299
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
300
        'OP_INSTANCE_REBOOT': 'STARTED'
301
    }
302

    
303
    # This dictionary contains the correspondence between
304
    # internal operating states and Server States as defined
305
    # by the Rackspace API.
306
    RSAPI_STATE_FROM_OPER_STATE = {
307
        "BUILD": "BUILD",
308
        "ERROR": "ERROR",
309
        "STOPPED": "STOPPED",
310
        "STARTED": "ACTIVE",
311
        "DESTROYED": "DELETED"
312
    }
313

    
314
    name = models.CharField('Virtual Machine Name', max_length=255)
315
    owner = models.ForeignKey(SynnefoUser)
316
    created = models.DateTimeField(auto_now_add=True)
317
    updated = models.DateTimeField(auto_now=True)
318
    charged = models.DateTimeField(default=datetime.datetime.now())
319
    sourceimage = models.ForeignKey("Image", null=False) 
320
    hostid = models.CharField(max_length=100)
321
    description = models.TextField()
322
    ipfour = models.IPAddressField()
323
    ipsix = models.CharField(max_length=100)
324
    flavor = models.ForeignKey(Flavor)
325
    deleted = models.BooleanField('Deleted', default=False)
326
    suspended = models.BooleanField('Administratively Suspended', default=False)
327

    
328
    # VM State 
329
    # The following fields are volatile data, in the sense
330
    # that they need not be persistent in the DB, but rather
331
    # get generated at runtime by quering Ganeti and applying
332
    # updates received from Ganeti.
333
    #
334
    # In the future they could be moved to a separate caching layer
335
    # and removed from the database.
336
    # [vkoukis] after discussion with [faidon].
337
    _action = models.CharField(choices=ACTIONS, max_length=30, null=True)
338
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
339
    _backendjobid = models.PositiveIntegerField(null=True)
340
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
341
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
342
    _backendlogmsg = models.TextField(null=True)
343

    
344
    # Error classes
345
    class InvalidBackendIdError(Exception):
346
         def __init__(self, value):
347
            self.value = value
348
         def __str__(self):
349
            return repr(self.value)
350

    
351
    class InvalidBackendMsgError(Exception):
352
         def __init__(self, opcode, status):
353
            self.opcode = opcode
354
            self.status = status
355
         def __str__(self):
356
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
357

    
358
    class InvalidActionError(Exception):
359
         def __init__(self, action):
360
            self._action = action
361
         def __str__(self):
362
            return repr(str(self._action))
363

    
364
    @staticmethod
365
    def id_from_instance_name(name):
366
        """Returns VirtualMachine's Django id, given a ganeti machine name.
367

368
        Strips the ganeti prefix atm. Needs a better name!
369
        
370
        """
371
        if not str(name).startswith(backend_prefix_id):
372
            raise VirtualMachine.InvalidBackendIdError(str(name))
373
        ns = str(name).lstrip(backend_prefix_id)
374
        if not ns.isdigit():
375
            raise VirtualMachine.InvalidBackendIdError(str(name))
376
        return int(ns)
377

    
378
    def __init__(self, *args, **kw):
379
        """Initialize state for just created VM instances."""
380
        super(VirtualMachine, self).__init__(*args, **kw)
381
        # This gets called BEFORE an instance gets save()d for
382
        # the first time.
383
        if not self.pk: 
384
            self._action = None
385
            self._backendjobid = None
386
            self._backendjobstatus = None
387
            self._backendopcode = None
388
            self._backendlogmsg = None
389
            # Do not use _update_state() for this, 
390
            # as this may cause save() to get called in __init__(),
391
            # breaking VirtualMachine.object.create() among other things.
392
            self._operstate = 'BUILD'
393

    
394
    def process_backend_msg(self, jobid, opcode, status, logmsg):
395
        """Process a job progress notification from the backend.
396

397
        Process an incoming message from the backend (currently Ganeti).
398
        Job notifications with a terminating status (sucess, error, or canceled),
399
        also update the operating state of the VM.
400

401
        """
402
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
403
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
404
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
405

    
406
        self._backendjobid = jobid
407
        self._backendjobstatus = status
408
        self._backendopcode = opcode
409
        self._backendlogmsg = logmsg
410

    
411
        # Notifications of success change the operating state
412
        if status == 'success':
413
            self._update_state(VirtualMachine.OPER_STATE_FROM_OPCODE[opcode])
414
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
415
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
416
            self._update_state('ERROR')
417
        # Any other notification of failure leaves the operating state unchanged
418

    
419
        self.save()
420

    
421
    def start_action(self, action):
422
        """Update the state of a VM when a new action is initiated."""
423
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
424
            raise VirtualMachine.InvalidActionError(action)
425

    
426
        # No actions to deleted and no actions beside destroy to suspended vms
427
        if self.deleted:
428
            raise VirtualMachine.InvalidActionError(action)
429

    
430
        self._action = action
431
        self._backendjobid = None
432
        self._backendopcode = None
433
        self._backendlogmsg = None
434

    
435
        # Update the relevant flags if the VM is being suspended or destroyed
436
        if action == "DESTROY":
437
            self.deleted = True
438
        elif action == "SUSPEND":
439
            self.suspended = True
440
        elif action == "START":
441
            self.suspended = False
442
        self.save()
443

    
444
    # FIXME: Perhaps move somewhere else, outside the model?
445
    def _get_rsapi_state(self):
446
        try:
447
            return VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
448
        except KeyError:
449
            return "UNKNOWN"
450

    
451
    rsapi_state = property(_get_rsapi_state)
452

    
453
    def _get_backend_id(self):
454
        """Returns the backend id for this VM by prepending backend-prefix."""
455
        return '%s%s' % (backend_prefix_id, str(self.id))
456

    
457
    backend_id = property(_get_backend_id)
458

    
459
    class Meta:
460
        verbose_name = u'Virtual machine instance'
461
        get_latest_by = 'created'
462
    
463
    def __unicode__(self):
464
        return self.name
465

    
466
    def _update_state(self, new_operstate):
467
        """Wrapper around updates of the _operstate field
468

469
        Currently calls the charge() method when necessary.
470

471
        """
472

    
473
        # Call charge() unconditionally before any change of
474
        # internal state.
475
        self.charge()
476
        self._operstate = new_operstate
477

    
478
    def charge(self):
479
        """Charges the owner of this VM.
480
        
481
        Charges the owner of a VM for the period
482
        from vm.charged to datetime.now(), based on the
483
        current operating state.
484
        
485
        """
486
        charged_states = ('STARTED', 'STOPPED')
487
       
488
        start_datetime = self.charged
489
        self.charged = datetime.datetime.now()
490

    
491
        # Only charge for a specific set of states
492
        if self._operstate not in charged_states:
493
            self.save()
494
            return
495

    
496
        cost_list = []
497

    
498
        if self._operstate == 'STARTED':
499
            cost_list = self.flavor.get_cost_active(start_datetime, self.charged)
500
        elif self._operstate == 'STOPPED':
501
            cost_list = self.flavor.get_cost_inactive(start_datetime, self.charged)
502

    
503
        total_cost = sum([x[1] for x in cost_list])
504
        # FIXME: This must happen inside a transaction.
505
        # Debiting the owner of this VM and storing a persistent value
506
        # for self.charged must happen ATOMICALLY.
507
        description = "Server = %d, charge = %d for state: %s" % (self.id, total_cost, self._operstate)
508
        self.owner.debit_account(total_cost, self, description)
509
        
510
        self.save()
511

    
512

    
513
class VirtualMachineGroup(models.Model):
514
    """Groups of VMs for SynnefoUsers"""
515
    name = models.CharField(max_length=255)
516
    created = models.DateTimeField('Time of creation', auto_now_add=True)
517
    updated = models.DateTimeField('Time of last update', auto_now=True)
518
    owner = models.ForeignKey(SynnefoUser)
519
    machines = models.ManyToManyField(VirtualMachine)
520

    
521
    class Meta:
522
        verbose_name = u'Virtual Machine Group'
523
        verbose_name_plural = 'Virtual Machine Groups'
524
        ordering = ['name']
525
    
526
    def __unicode__(self):
527
        return self.name
528

    
529

    
530
class VirtualMachineMetadata(models.Model):
531
    meta_key = models.CharField(max_length=50)
532
    meta_value = models.CharField(max_length=500)
533
    vm = models.ForeignKey(VirtualMachine)
534
    
535
    class Meta:
536
        verbose_name = u'Key-value pair of metadata for a VM.'
537
    
538
    def __unicode__(self):
539
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
540

    
541

    
542
class Debit(models.Model):
543
    when = models.DateTimeField()
544
    user = models.ForeignKey(SynnefoUser)
545
    vm = models.ForeignKey(VirtualMachine)
546
    description = models.TextField()
547
    
548
    class Meta:
549
        verbose_name = u'Accounting log'
550

    
551
    def __unicode__(self):
552
        return u'%s - %s - %s - %s' % ( self.user.id, self.vm.name, str(self.when), self.description)
553

    
554

    
555
class Disk(models.Model):
556
    name = models.CharField(max_length=255)
557
    created = models.DateTimeField('Time of creation', auto_now_add=True)
558
    updated = models.DateTimeField('Time of last update', auto_now=True)
559
    size = models.PositiveIntegerField('Disk size in GBs')
560
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
561
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
562

    
563
    class Meta:
564
        verbose_name = u'Disk instance'
565

    
566
    def __unicode__(self):
567
        return self.name