Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 22e52ede

History | View | Annotate | Download (14.7 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
from logic import utils, credits
9

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

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

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

    
48

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

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

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

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

    
72

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

    
84

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

    
101

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

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

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

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

    
128
        return 0
129

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

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

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

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

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

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

    
154
        # Get the related FlavorCost objects, sorted.
155
        price_list = FlavorCost.objects.filter(flavor=self).order_by('effective_from')
156

    
157
        # add the extra field FlavorCost.effective_to
158
        for idx in range(0, len(price_list)):
159
            if idx + 1 == len(price_list):
160
                price_list[idx].effective_to = None
161
            else:
162
                price_list[idx].effective_to = price_list[idx + 1].effective_from
163

    
164
        price_result = []
165
        found_start = False
166

    
167
        # Find the affected FlavorCost, according to the
168
        # dates, and put them in price_result
169
        for p in price_list:
170
            if between(p, start_datetime):
171
                found_start = True
172
                p.effective_from = start_datetime
173
            if between(p, end_datetime):
174
                p.effective_to = end_datetime
175
                price_result.append(p)
176
                break
177
            if found_start:
178
                price_result.append(p)
179

    
180
        results = []
181

    
182
        # Create the list and the result tuples
183
        for p in price_result:
184
            if active:
185
                cost = p.cost_active
186
            else:
187
                cost = p.cost_inactive
188

    
189
            results.append( ( p.effective_from, utils.calculate_cost(p.effective_from, p.effective_to, cost)) )
190

    
191
        return results
192

    
193
    def get_cost_active(self, start_datetime, end_datetime):
194
        """Returns a list with the active costs for the specified duration"""
195
        return self._get_costs(start_datetime, end_datetime, True)
196

    
197
    def get_cost_inactive(self, start_datetime, end_datetime):
198
        """Returns a list with the inactive costs for the specified duration"""
199
        return self._get_costs(start_datetime, end_datetime, False)
200

    
201

    
202
class FlavorCost(models.Model):
203
    cost_active = models.PositiveIntegerField('Active Cost')
204
    cost_inactive = models.PositiveIntegerField('Inactive Cost')
205
    effective_from = models.DateTimeField()
206
    flavor = models.ForeignKey(Flavor)
207
    
208
    class Meta:
209
        verbose_name = u'Pricing history for flavors'
210
    
211
    def __unicode__(self):
212
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
213

    
214

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

    
252
    # The operating state of a VM,
253
    # upon the successful completion of a backend operation.
254
    OPER_STATE_FROM_OPCODE = {
255
        'OP_INSTANCE_CREATE': 'STARTED',
256
        'OP_INSTANCE_REMOVE': 'DESTROYED',
257
        'OP_INSTANCE_STARTUP': 'STARTED',
258
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
259
        'OP_INSTANCE_REBOOT': 'STARTED'
260
    }
261

    
262
    # This dictionary contains the correspondence between
263
    # internal operating states and Server States as defined
264
    # by the Rackspace API.
265
    RSAPI_STATE_FROM_OPER_STATE = {
266
        "BUILD": "BUILD",
267
        "ERROR": "ERROR",
268
        "STOPPED": "STOPPED",
269
        "STARTED": "ACTIVE",
270
        "DESTROYED": "DELETED"
271
    }
272

    
273
    name = models.CharField('Virtual Machine Name', max_length=255)
274
    owner = models.ForeignKey(SynnefoUser)
275
    created = models.DateTimeField(auto_now_add=True)
276
    updated = models.DateTimeField(auto_now=True)
277
    charged = models.DateTimeField(default=datetime.datetime.now())
278
    sourceimage = models.ForeignKey("Image", null=False) 
279
    hostid = models.CharField(max_length=100)
280
    description = models.TextField()
281
    ipfour = models.IPAddressField()
282
    ipsix = models.CharField(max_length=100)
283
    flavor = models.ForeignKey(Flavor)
284
    deleted = models.BooleanField('Deleted', default=False)
285
    suspended = models.BooleanField('Administratively Suspended', default=False)
286

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

    
303
    # Error classes
304
    class InvalidBackendIdError(Exception):
305
         def __init__(self, value):
306
            self.value = value
307
         def __str__(self):
308
            return repr(self.value)
309

    
310
    class InvalidBackendMsgError(Exception):
311
         def __init__(self, opcode, status):
312
            self.opcode = opcode
313
            self.status = status
314
         def __str__(self):
315
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
316

    
317
    class InvalidActionError(Exception):
318
         def __init__(self, action):
319
            self._action = action
320
         def __str__(self):
321
            return repr(str(self._action))
322

    
323
    def __init__(self, *args, **kw):
324
        """Initialize state for just created VM instances."""
325
        super(VirtualMachine, self).__init__(*args, **kw)
326
        # This gets called BEFORE an instance gets save()d for
327
        # the first time.
328
        if not self.pk: 
329
            self._action = None
330
            self._backendjobid = None
331
            self._backendjobstatus = None
332
            self._backendopcode = None
333
            self._backendlogmsg = None
334
            # Do not use _update_state() for this, 
335
            # as this may cause save() to get called in __init__(),
336
            # breaking VirtualMachine.object.create() among other things.
337
            self._operstate = 'BUILD'
338

    
339
    # FIXME: leave this here to preserve the property rsapistate
340
    def _get_rsapi_state(self):
341
        return utils.get_rsapi_state(self)
342

    
343
    rsapi_state = property(_get_rsapi_state)
344

    
345
    def _get_backend_id(self):
346
        """Returns the backend id for this VM by prepending backend-prefix."""
347
        return '%s%s' % (settings.BACKEND_PREFIX_ID, str(self.id))
348

    
349
    backend_id = property(_get_backend_id)
350

    
351
    class Meta:
352
        verbose_name = u'Virtual machine instance'
353
        get_latest_by = 'created'
354
    
355
    def __unicode__(self):
356
        return self.name
357

    
358
    def _update_state(self, new_operstate):
359
        """Wrapper around updates of the _operstate field
360

361
        Currently calls the charge() method when necessary.
362

363
        """
364

    
365
        # Call charge() unconditionally before any change of
366
        # internal state.
367
        credits.charge(self)
368
        self._operstate = new_operstate
369

    
370

    
371
class VirtualMachineGroup(models.Model):
372
    """Groups of VMs for SynnefoUsers"""
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
    owner = models.ForeignKey(SynnefoUser)
377
    machines = models.ManyToManyField(VirtualMachine)
378

    
379
    class Meta:
380
        verbose_name = u'Virtual Machine Group'
381
        verbose_name_plural = 'Virtual Machine Groups'
382
        ordering = ['name']
383
    
384
    def __unicode__(self):
385
        return self.name
386

    
387

    
388
class VirtualMachineMetadata(models.Model):
389
    meta_key = models.CharField(max_length=50)
390
    meta_value = models.CharField(max_length=500)
391
    vm = models.ForeignKey(VirtualMachine)
392
    
393
    class Meta:
394
        verbose_name = u'Key-value pair of metadata for a VM.'
395
    
396
    def __unicode__(self):
397
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
398

    
399

    
400
class Debit(models.Model):
401
    when = models.DateTimeField()
402
    user = models.ForeignKey(SynnefoUser)
403
    vm = models.ForeignKey(VirtualMachine)
404
    description = models.TextField()
405
    
406
    class Meta:
407
        verbose_name = u'Accounting log'
408

    
409
    def __unicode__(self):
410
        return u'%s - %s - %s - %s' % ( self.user.id, self.vm.name, str(self.when), self.description)
411

    
412

    
413
class Disk(models.Model):
414
    name = models.CharField(max_length=255)
415
    created = models.DateTimeField('Time of creation', auto_now_add=True)
416
    updated = models.DateTimeField('Time of last update', auto_now=True)
417
    size = models.PositiveIntegerField('Disk size in GBs')
418
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
419
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
420

    
421
    class Meta:
422
        verbose_name = u'Disk instance'
423

    
424
    def __unicode__(self):
425
        return self.name