Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 92c53da1

History | View | Annotate | Download (17.9 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
    @staticmethod
332
    def id_from_instance_name(name):
333
        """Returns VirtualMachine's Django id, given a ganeti machine name.
334

335
        Strips the ganeti prefix atm. Needs a better name!
336
        
337
        """
338
        if not str(name).startswith(settings.BACKEND_PREFIX_ID):
339
            raise VirtualMachine.InvalidBackendIdError(str(name))
340
        ns = str(name).lstrip(settings.BACKEND_PREFIX_ID)
341
        if not ns.isdigit():
342
            raise VirtualMachine.InvalidBackendIdError(str(name))
343
        return int(ns)
344

    
345
    def __init__(self, *args, **kw):
346
        """Initialize state for just created VM instances."""
347
        super(VirtualMachine, self).__init__(*args, **kw)
348
        # This gets called BEFORE an instance gets save()d for
349
        # the first time.
350
        if not self.pk: 
351
            self._action = None
352
            self._backendjobid = None
353
            self._backendjobstatus = None
354
            self._backendopcode = None
355
            self._backendlogmsg = None
356
            # Do not use _update_state() for this, 
357
            # as this may cause save() to get called in __init__(),
358
            # breaking VirtualMachine.object.create() among other things.
359
            self._operstate = 'BUILD'
360

    
361
    def process_backend_msg(self, jobid, opcode, status, logmsg):
362
        """Process a job progress notification from the backend.
363

364
        Process an incoming message from the backend (currently Ganeti).
365
        Job notifications with a terminating status (sucess, error, or canceled),
366
        also update the operating state of the VM.
367

368
        """
369
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
370
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
371
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
372

    
373
        self._backendjobid = jobid
374
        self._backendjobstatus = status
375
        self._backendopcode = opcode
376
        self._backendlogmsg = logmsg
377

    
378
        # Notifications of success change the operating state
379
        if status == 'success':
380
            self._update_state(VirtualMachine.OPER_STATE_FROM_OPCODE[opcode])
381
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
382
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
383
            self._update_state('ERROR')
384
        # Any other notification of failure leaves the operating state unchanged
385

    
386
        self.save()
387

    
388
    def start_action(self, action):
389
        """Update the state of a VM when a new action is initiated."""
390
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
391
            raise VirtualMachine.InvalidActionError(action)
392

    
393
        # No actions to deleted and no actions beside destroy to suspended VMs
394
        if self.deleted:
395
            raise VirtualMachine.InvalidActionError(action)
396

    
397
        self._action = action
398
        self._backendjobid = None
399
        self._backendopcode = None
400
        self._backendjobstatus = None
401
        self._backendlogmsg = None
402

    
403
        # Update the relevant flags if the VM is being suspended or destroyed
404
        if action == "DESTROY":
405
            self.deleted = True
406
        elif action == "SUSPEND":
407
            self.suspended = True
408
        elif action == "START":
409
            self.suspended = False
410
        self.save()
411

    
412
    # FIXME: Perhaps move somewhere else, outside the model?
413
    def _get_rsapi_state(self):
414
        try:
415
            r = VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
416
        except KeyError:
417
            return "UNKNOWN"
418
        # A machine is in REBOOT if an OP_INSTANCE_REBOOT request is in progress
419
        if r == 'ACTIVE' and self._backendopcode == 'OP_INSTANCE_REBOOT' and \
420
            self._backendjobstatus in ('queued', 'waiting', 'running'):
421
            return "REBOOT"
422
        return r 
423

    
424
    rsapi_state = property(_get_rsapi_state)
425

    
426
    def _get_backend_id(self):
427
        """Returns the backend id for this VM by prepending backend-prefix."""
428
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
429

    
430
    backend_id = property(_get_backend_id)
431

    
432
    class Meta:
433
        verbose_name = u'Virtual machine instance'
434
        get_latest_by = 'created'
435
    
436
    def __unicode__(self):
437
        return self.name
438

    
439
    def _update_state(self, new_operstate):
440
        """Wrapper around updates of the _operstate field
441

442
        Currently calls the charge() method when necessary.
443

444
        """
445

    
446
        # Call charge() unconditionally before any change of
447
        # internal state.
448
        self.charge()
449
        self._operstate = new_operstate
450

    
451

    
452
class VirtualMachineGroup(models.Model):
453
    """Groups of VMs for SynnefoUsers"""
454
    name = models.CharField(max_length=255)
455
    created = models.DateTimeField('Time of creation', auto_now_add=True)
456
    updated = models.DateTimeField('Time of last update', auto_now=True)
457
    owner = models.ForeignKey(SynnefoUser)
458
    machines = models.ManyToManyField(VirtualMachine)
459

    
460
    class Meta:
461
        verbose_name = u'Virtual Machine Group'
462
        verbose_name_plural = 'Virtual Machine Groups'
463
        ordering = ['name']
464
    
465
    def __unicode__(self):
466
        return self.name
467

    
468

    
469
class VirtualMachineMetadata(models.Model):
470
    meta_key = models.CharField(max_length=50)
471
    meta_value = models.CharField(max_length=500)
472
    vm = models.ForeignKey(VirtualMachine)
473
    
474
    class Meta:
475
        verbose_name = u'Key-value pair of metadata for a VM.'
476
    
477
    def __unicode__(self):
478
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
479

    
480

    
481
class Debit(models.Model):
482
    when = models.DateTimeField()
483
    user = models.ForeignKey(SynnefoUser)
484
    vm = models.ForeignKey(VirtualMachine)
485
    description = models.TextField()
486
    
487
    class Meta:
488
        verbose_name = u'Accounting log'
489

    
490
    def __unicode__(self):
491
        return u'%s - %s - %s - %s' % ( self.user.id, self.vm.name, str(self.when), self.description)
492

    
493

    
494
class Disk(models.Model):
495
    name = models.CharField(max_length=255)
496
    created = models.DateTimeField('Time of creation', auto_now_add=True)
497
    updated = models.DateTimeField('Time of last update', auto_now=True)
498
    size = models.PositiveIntegerField('Disk size in GBs')
499
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
500
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
501

    
502
    class Meta:
503
        verbose_name = u'Disk instance'
504

    
505
    def __unicode__(self):
506
        return self.name