Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ e3a99a08

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

    
9
class SynnefoUser(models.Model):
10
    name = models.CharField('Synnefo Username', max_length=255)
11
    credit = models.IntegerField('Credit Balance')
12
    created = models.DateTimeField('Time of creation', auto_now_add=True)
13
    updated = models.DateTimeField('Time of last update', auto_now=True)
14

    
15
    class Meta:
16
        verbose_name = u'Synnefo User'
17
    
18
    def __unicode__(self):
19
        return self.name 
20

    
21
    def get_limit(self, limit_name):
22
        """Returns the limit value for the specified limit"""
23
        limit_objs = Limit.objects.filter(name=limit_name, user=self)        
24
        if len(limit_objs) == 1:
25
            return limit_objs[0].value
26
        
27
        return 0
28
        
29
    def _get_credit_quota(self):
30
        """Internal getter function for credit quota"""
31
        return self.get_limit('QUOTA_CREDIT')
32
        
33
    credit_quota = property(_get_credit_quota)
34
    
35
    def _get_monthly_rate(self):
36
        """Internal getter function for monthly credit issue rate"""
37
        return self.get_limit('MONTHLY_RATE')
38
        
39
    monthly_rate = property(_get_monthly_rate)
40
    
41
    def _get_min_credits(self):
42
        """Internal getter function for maximum number of violations"""
43
        return self.get_limit('MIN_CREDITS')
44
        
45
    min_credits = property(_get_min_credits)
46

    
47

    
48
class Image(models.Model):
49
    # This is WIP, FIXME
50
    IMAGE_STATES = (
51
        ('ACTIVE', 'Active'),
52
        ('SAVING', 'Saving'),
53
        ('DELETED', 'Deleted')
54
    )
55

    
56
    name = models.CharField('Image name', max_length=255)
57
    state = models.CharField('Current Image State', choices=IMAGE_STATES, max_length=30)
58
    description = models.TextField('General description')
59
    size = models.PositiveIntegerField('Image size in MBs')
60
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)
61
    created = models.DateTimeField('Time of creation', auto_now_add=True)
62
    updated = models.DateTimeField('Time of last update', auto_now=True)
63
    sourcevm = models.ForeignKey("VirtualMachine", null=True)
64

    
65
    class Meta:
66
        verbose_name = u'Image'
67

    
68
    def __unicode__(self):
69
        return u'%s' % ( self.name, )
70

    
71

    
72
class ImageMetadata(models.Model):
73
    meta_key = models.CharField('Image metadata key name', max_length=50)
74
    meta_value = models.CharField('Image metadata value', max_length=500)
75
    image = models.ForeignKey(Image)
76
    
77
    class Meta:
78
        verbose_name = u'Key-value pair of Image metadata.'
79
    
80
    def __unicode__(self):
81
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.image.name)
82

    
83

    
84
class Limit(models.Model):
85
    LIMITS = (
86
        ('QUOTA_CREDIT', 'Maximum amount of credits per user'),
87
        ('MIN_CREDITS', 'Minimum amount of credits per user'),
88
        ('MONTHLY_RATE', 'Monthly credit issue rate')
89
    )
90
    user = models.ForeignKey(SynnefoUser)
91
    name = models.CharField('Limit key name', choices=LIMITS, max_length=30, null=False)
92
    value = models.IntegerField('Limit current value')
93
    
94
    class Meta:
95
        verbose_name = u'Enforced limit for user'
96
    
97
    def __unicode__(self):
98
        return u'Limit %s for user %s: %d' % (self.value, self.user, self.value)
99

    
100

    
101
class Flavor(models.Model):
102
    cpu = models.IntegerField('Number of CPUs', default=0)
103
    ram = models.IntegerField('Size of RAM', default=0)
104
    disk = models.IntegerField('Size of Disk space', default=0)
105
    
106
    class Meta:
107
        verbose_name = u'Virtual machine flavor'
108
        unique_together = ("cpu","ram","disk")
109
            
110
    def _get_name(self):
111
        """Returns flavor name (generated)"""
112
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
113

    
114
    def _current_cost(self, active):
115
        """Returns active/inactive cost value
116

117
        set active = True to get active cost and False for the inactive.
118

119
        """
120
        fch_list = FlavorCost.objects.filter(flavor=self).order_by('-effective_from')
121
        if len(fch_list) > 0:
122
            if active:
123
                return fch_list[0].cost_active
124
            else:
125
                return fch_list[0].cost_inactive
126

    
127
        return 0
128

    
129
    def _current_cost_active(self):
130
        """Returns current active cost (property method)"""
131
        return self._current_cost(True)
132

    
133
    def _current_cost_inactive(self):
134
        """Returns current inactive cost (property method)"""
135
        return self._current_cost(False)
136

    
137
    name = property(_get_name)
138
    current_cost_active = property(_current_cost_active)
139
    current_cost_inactive = property(_current_cost_inactive)
140

    
141
    def __unicode__(self):
142
        return self.name
143

    
144
    def _get_costs(self, start_datetime, end_datetime, active):
145
        """Return a list with FlavorCost objects for the specified duration"""
146
        def between(enh_fc, a_date):
147
            """Checks if a date is between a FlavorCost duration"""
148
            if enh_fc.effective_from <= a_date and enh_fc.effective_to is None:
149
                return True
150

    
151
            return enh_fc.effective_from <= a_date and enh_fc.effective_to >= a_date
152

    
153
        def calculate_cost(start_date, end_date, cost):
154
            """Calculate the total cost for the specified duration"""
155
            td = end_date - start_date
156
            sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
157
            total_hours = float(sec) / float(60.0*60.0)
158
            total_cost = float(cost)*total_hours
159

    
160
            return round(total_cost)
161

    
162
        # Get the related FlavorCost objects, sorted.
163
        price_list = FlavorCost.objects.filter(flavor=self).order_by('effective_from')
164

    
165
        # add the extra field FlavorCost.effective_to
166
        for idx in range(0, len(price_list)):
167
            if idx + 1 == len(price_list):
168
                price_list[idx].effective_to = None
169
            else:
170
                price_list[idx].effective_to = price_list[idx + 1].effective_from
171

    
172
        price_result = []
173
        found_start = False
174

    
175
        # Find the affected FlavorCost, according to the
176
        # dates, and put them in price_result
177
        for p in price_list:
178
            if between(p, start_datetime):
179
                found_start = True
180
                p.effective_from = start_datetime
181
            if between(p, end_datetime):
182
                p.effective_to = end_datetime
183
                price_result.append(p)
184
                break
185
            if found_start:
186
                price_result.append(p)
187

    
188
        results = []
189

    
190
        # Create the list and the result tuples
191
        for p in price_result:
192
            if active:
193
                cost = p.cost_active
194
            else:
195
                cost = p.cost_inactive
196

    
197
            results.append( ( p.effective_from, calculate_cost(p.effective_from, p.effective_to, cost)) )
198

    
199
        return results
200

    
201
    def get_cost_active(self, start_datetime, end_datetime):
202
        """Returns a list with the active costs for the specified duration"""
203
        return self._get_costs(start_datetime, end_datetime, True)
204

    
205
    def get_cost_inactive(self, start_datetime, end_datetime):
206
        """Returns a list with the inactive costs for the specified duration"""
207
        return self._get_costs(start_datetime, end_datetime, False)
208

    
209

    
210
class FlavorCost(models.Model):
211
    cost_active = models.PositiveIntegerField('Active Cost')
212
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
213
    effective_from = models.DateTimeField()
214
    flavor = models.ForeignKey(Flavor)
215
    
216
    class Meta:
217
        verbose_name = u'Pricing history for flavors'
218
    
219
    def __unicode__(self):
220
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
221

    
222

    
223
class VirtualMachine(models.Model):
224
    # The list of possible actions for a VM
225
    ACTIONS = (
226
       ('CREATE', 'Create VM'),
227
       ('START', 'Start VM'),
228
       ('STOP', 'Shutdown VM'),
229
       ('SUSPEND', 'Admin Suspend VM'),
230
       ('REBOOT', 'Reboot VM'),
231
       ('DESTROY', 'Destroy VM')
232
    )
233
    # The internal operating state of a VM
234
    OPER_STATES = (
235
        ('BUILD', 'Queued for creation'),
236
        ('ERROR', 'Creation failed'),
237
        ('STOPPED', 'Stopped'),
238
        ('STARTED', 'Started'),
239
        ('DESTROYED', 'Destroyed')
240
    )
241
    # The list of possible operations on the backend
242
    BACKEND_OPCODES = (
243
        ('OP_INSTANCE_CREATE', 'Create Instance'),
244
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
245
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
246
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
247
        ('OP_INSTANCE_REBOOT', 'Reboot Instance')
248
    )
249
    # A backend job may be in one of the following possible states
250
    BACKEND_STATUSES = (
251
        ('queued', 'request queued'),
252
        ('waiting', 'request waiting for locks'),
253
        ('canceling', 'request being canceled'),
254
        ('running', 'request running'),
255
        ('canceled', 'request canceled'),
256
        ('success', 'request completed successfully'),
257
        ('error', 'request returned error')
258
    )
259

    
260
    # The operating state of a VM,
261
    # upon the successful completion of a backend operation.
262
    OPER_STATE_FROM_OPCODE = {
263
        'OP_INSTANCE_CREATE': 'STARTED',
264
        'OP_INSTANCE_REMOVE': 'DESTROYED',
265
        'OP_INSTANCE_STARTUP': 'STARTED',
266
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
267
        'OP_INSTANCE_REBOOT': 'STARTED'
268
    }
269

    
270
    # This dictionary contains the correspondence between
271
    # internal operating states and Server States as defined
272
    # by the Rackspace API.
273
    RSAPI_STATE_FROM_OPER_STATE = {
274
        "BUILD": "BUILD",
275
        "ERROR": "ERROR",
276
        "STOPPED": "STOPPED",
277
        "STARTED": "ACTIVE",
278
        "DESTROYED": "DELETED"
279
    }
280

    
281
    name = models.CharField('Virtual Machine Name', max_length=255)
282
    owner = models.ForeignKey(SynnefoUser)
283
    created = models.DateTimeField(auto_now_add=True)
284
    updated = models.DateTimeField(auto_now=True)
285
    charged = models.DateTimeField(default=datetime.datetime.now())
286
    sourceimage = models.ForeignKey("Image", null=False) 
287
    hostid = models.CharField(max_length=100)
288
    description = models.TextField()
289
    ipfour = models.IPAddressField()
290
    ipsix = models.CharField(max_length=100)
291
    flavor = models.ForeignKey(Flavor)
292
    deleted = models.BooleanField('Deleted', default=False)
293
    suspended = models.BooleanField('Administratively Suspended', default=False)
294

    
295
    # VM State 
296
    # The following fields are volatile data, in the sense
297
    # that they need not be persistent in the DB, but rather
298
    # get generated at runtime by quering Ganeti and applying
299
    # updates received from Ganeti.
300
    #
301
    # In the future they could be moved to a separate caching layer
302
    # and removed from the database.
303
    # [vkoukis] after discussion with [faidon].
304
    _action = models.CharField(choices=ACTIONS, max_length=30, null=True)
305
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
306
    _backendjobid = models.PositiveIntegerField(null=True)
307
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
308
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
309
    _backendlogmsg = models.TextField(null=True)
310

    
311
    # Error classes
312
    class InvalidBackendIdError(Exception):
313
         def __init__(self, value):
314
            self.value = value
315
         def __str__(self):
316
            return repr(self.value)
317

    
318
    class InvalidBackendMsgError(Exception):
319
         def __init__(self, opcode, status):
320
            self.opcode = opcode
321
            self.status = status
322
         def __str__(self):
323
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
324

    
325
    class InvalidActionError(Exception):
326
         def __init__(self, action):
327
            self._action = action
328
         def __str__(self):
329
            return repr(str(self._action))
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
            # Do not use _update_state() for this, 
343
            # as this may cause save() to get called in __init__(),
344
            # breaking VirtualMachine.object.create() among other things.
345
            self._operstate = 'BUILD'
346

    
347
    def process_backend_msg(self, jobid, opcode, status, logmsg):
348
        """Process a job progress notification from the backend.
349

350
        Process an incoming message from the backend (currently Ganeti).
351
        Job notifications with a terminating status (sucess, error, or canceled),
352
        also update the operating state of the VM.
353

354
        """
355
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
356
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
357
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
358

    
359
        self._backendjobid = jobid
360
        self._backendjobstatus = status
361
        self._backendopcode = opcode
362
        self._backendlogmsg = logmsg
363

    
364
        # Notifications of success change the operating state
365
        if status == 'success':
366
            self._update_state(VirtualMachine.OPER_STATE_FROM_OPCODE[opcode])
367
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
368
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
369
            self._update_state('ERROR')
370
        # Any other notification of failure leaves the operating state unchanged
371

    
372
        self.save()
373

    
374
    def start_action(self, action):
375
        """Update the state of a VM when a new action is initiated."""
376
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
377
            raise VirtualMachine.InvalidActionError(action)
378

    
379
        # No actions to deleted and no actions beside destroy to suspended VMs
380
        if self.deleted:
381
            raise VirtualMachine.InvalidActionError(action)
382

    
383
        self._action = action
384
        self._backendjobid = None
385
        self._backendopcode = None
386
        self._backendjobstatus = None
387
        self._backendlogmsg = None
388

    
389
        # Update the relevant flags if the VM is being suspended or destroyed
390
        if action == "DESTROY":
391
            self.deleted = True
392
        elif action == "SUSPEND":
393
            self.suspended = True
394
        elif action == "START":
395
            self.suspended = False
396
        self.save()
397

    
398
    # FIXME: Perhaps move somewhere else, outside the model?
399
    def _get_rsapi_state(self):
400
        try:
401
            r = VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
402
        except KeyError:
403
            return "UNKNOWN"
404
        # A machine is in REBOOT if an OP_INSTANCE_REBOOT request is in progress
405
        if r == 'ACTIVE' and self._backendopcode == 'OP_INSTANCE_REBOOT' and \
406
            self._backendjobstatus in ('queued', 'waiting', 'running'):
407
            return "REBOOT"
408
        return r 
409

    
410
    rsapi_state = property(_get_rsapi_state)
411

    
412
    def _get_backend_id(self):
413
        """Returns the backend id for this VM by prepending backend-prefix."""
414
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
415

    
416
    backend_id = property(_get_backend_id)
417

    
418
    class Meta:
419
        verbose_name = u'Virtual machine instance'
420
        get_latest_by = 'created'
421
    
422
    def __unicode__(self):
423
        return self.name
424

    
425
    def _update_state(self, new_operstate):
426
        """Wrapper around updates of the _operstate field
427

428
        Currently calls the charge() method when necessary.
429

430
        """
431

    
432
        # Call charge() unconditionally before any change of
433
        # internal state.
434
        self.charge()
435
        self._operstate = new_operstate
436

    
437

    
438
class VirtualMachineGroup(models.Model):
439
    """Groups of VMs for SynnefoUsers"""
440
    name = models.CharField(max_length=255)
441
    created = models.DateTimeField('Time of creation', auto_now_add=True)
442
    updated = models.DateTimeField('Time of last update', auto_now=True)
443
    owner = models.ForeignKey(SynnefoUser)
444
    machines = models.ManyToManyField(VirtualMachine)
445

    
446
    class Meta:
447
        verbose_name = u'Virtual Machine Group'
448
        verbose_name_plural = 'Virtual Machine Groups'
449
        ordering = ['name']
450
    
451
    def __unicode__(self):
452
        return self.name
453

    
454

    
455
class VirtualMachineMetadata(models.Model):
456
    meta_key = models.CharField(max_length=50)
457
    meta_value = models.CharField(max_length=500)
458
    vm = models.ForeignKey(VirtualMachine)
459
    
460
    class Meta:
461
        verbose_name = u'Key-value pair of metadata for a VM.'
462
    
463
    def __unicode__(self):
464
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
465

    
466

    
467
class Debit(models.Model):
468
    when = models.DateTimeField()
469
    user = models.ForeignKey(SynnefoUser)
470
    vm = models.ForeignKey(VirtualMachine)
471
    description = models.TextField()
472
    
473
    class Meta:
474
        verbose_name = u'Accounting log'
475

    
476
    def __unicode__(self):
477
        return u'%s - %s - %s - %s' % ( self.user.id, self.vm.name, str(self.when), self.description)
478

    
479

    
480
class Disk(models.Model):
481
    name = models.CharField(max_length=255)
482
    created = models.DateTimeField('Time of creation', auto_now_add=True)
483
    updated = models.DateTimeField('Time of last update', auto_now=True)
484
    size = models.PositiveIntegerField('Disk size in GBs')
485
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
486
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
487

    
488
    class Meta:
489
        verbose_name = u'Disk instance'
490

    
491
    def __unicode__(self):
492
        return self.name