Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 9071888e

History | View | Annotate | Download (15 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 Limit(models.Model):
13
    description = models.CharField(max_length=45)
14
    
15
    class Meta:
16
        verbose_name = u'User limit'
17
    
18
    def __unicode__(self):
19
        return self.description
20

    
21

    
22
class SynnefoUser(models.Model):
23
    name = models.CharField(max_length=255)
24
    credit = models.IntegerField()
25
    quota = models.IntegerField()
26
    created = models.DateField()
27
    monthly_rate = models.IntegerField()
28
    user = models.ForeignKey(User)
29
    limits = models.ManyToManyField(Limit, through='UserLimit')
30
    violations = models.IntegerField()
31
    max_violations = models.IntegerField(default=3)
32
    
33
    class Meta:
34
        verbose_name = u'Synnefo User'
35
    
36
    def __unicode__(self):
37
        return self.name
38
    
39
    def charge_credits(self, cost, start, end):
40
        """Reduce user credits for specified duration.
41
        
42
        Returns amount of credits remaining. Negative if the user has surpassed his limit.
43
        
44
        """
45
        td = end - start
46
        sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
47
        
48
        total_hours = float(sec) / float(60.0*60.0)
49
        total_cost = float(cost)*total_hours
50
        
51
        self.credit = self.credit - round(total_cost)
52
        
53
        if self.credit < 0:
54
            self.violations = self.violations + 1
55
        else:
56
            self.violations = 0
57
                
58
        return self.credit
59
    
60
    def allocate_credits(self):
61
        """Allocate credits. Add monthly rate to user credit reserve."""
62
        self.credit = self.credit + self.monthly_rate
63
        
64
        # ensure that the user has not more credits than his quota
65
        if self.credit > self.quota:
66
            self.credit = self.quota
67

    
68

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

    
77
    name = models.CharField(max_length=255, help_text=_('description'))
78
    updated = models.DateTimeField(help_text=_("Image update date"))
79
    created = models.DateTimeField(help_text=_("Image creation date"), default=datetime.datetime.now())
80
    state = models.CharField(choices=IMAGE_STATES, max_length=30)
81
    description = models.TextField(help_text=_('description'))
82
    owner = models.ForeignKey(SynnefoUser,blank=True, null=True)
83
    #FIXME: ImageMetadata, as in VirtualMachineMetadata
84
    #       "os" contained in metadata. Newly created Server inherits value of "os" metadata key from Image.
85
    #       The Web UI uses the value of "os" to determine the icon to use.
86

    
87
    class Meta:
88
        verbose_name = u'Image'
89

    
90
    def __unicode__(self):
91
        return u'%s' % (self.name)
92

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

    
103
    vm_id = property(get_vmid)
104

    
105

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

    
117

    
118
class UserLimit(models.Model):
119
    user = models.ForeignKey(SynnefoUser)
120
    limit = models.ForeignKey(Limit)
121
    value = models.IntegerField()
122
    
123
    class Meta:
124
        unique_together = ('user', 'limit')
125
        verbose_name = u'Enforced limit for user'
126
    
127
    def __unicode__(self):
128
        return u'Limit %s for user %s: %d' % (self.limit, self.user, self.value)
129

    
130

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

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

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

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

    
169
    def __unicode__(self):
170
        return self.name
171
    
172
    def get_price_list(self):
173
        """Returns the price catalog for this Flavor"""
174
        fch_list = FlavorCostHistory.objects.filter(flavor=self).order_by('effective_from')
175
        
176
        return fch_list            
177

    
178

    
179
class FlavorCostHistory(models.Model):
180
    cost_active = models.PositiveIntegerField()
181
    cost_inactive = models.PositiveIntegerField()
182
    effective_from = models.DateField()
183
    flavor = models.ForeignKey(Flavor)
184
    
185
    class Meta:
186
        verbose_name = u'Pricing history for flavors'
187
    
188
    def __unicode__(self):
189
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
190
        
191
    @staticmethod
192
    def find_cost(fch_list, dat):
193
        rdate = fch_list[0]
194

    
195
        for fc in fch_list:
196
            if dat > fc.effective_from:
197
                rdate = fc
198
        
199
        return rdate
200

    
201

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

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

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

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

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

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

    
256
    name = models.CharField(max_length=255)
257
    created = models.DateTimeField(help_text=_('VM creation date'), default=datetime.datetime.now())
258
    charged = models.DateTimeField()
259
    # Use string reference to avoid circular ForeignKey def.
260
    # FIXME: "sourceimage" works, "image" causes validation errors. See "related_name" in the Django docs.
261
    sourceimage = models.ForeignKey(Image, null=False) 
262
    hostid = models.CharField(max_length=100)
263
    description = models.TextField(help_text=_('description'))
264
    ipfour = models.IPAddressField()
265
    ipsix = models.CharField(max_length=100)
266
    flavor = models.ForeignKey(Flavor)
267
    suspended = models.BooleanField('Administratively Suspended')
268

    
269
    # VM State [volatile data]
270
    updated = models.DateTimeField(null=True)
271
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
272
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
273
    _backendjobid = models.PositiveIntegerField(null=True)
274
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
275
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
276
    _backendlogmsg = models.TextField(null=True)
277

    
278
    # Error classes
279
    class InvalidBackendIdError(Exception):
280
         def __init__(self, value):
281
            self.value = value
282
         def __str__(self):
283
            return repr(self.value)
284

    
285
    class InvalidBackendMsgError(Exception):
286
         def __init__(self, opcode, status):
287
            self.opcode = opcode
288
            self.status = status
289
         def __str__(self):
290
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
291

    
292
    class InvalidActionError(Exception):
293
         def __init__(self, action):
294
            self.__action = action
295
         def __str__(self):
296
            return repr(str(self._action))
297

    
298
    @staticmethod
299
    def id_from_instance_name(name):
300
        """Returns VirtualMachine's Django id, given a ganeti machine name.
301

302
        Strips the ganeti prefix atm. Needs a better name!
303
        
304
        """
305
        if not str(name).startswith(backend_prefix_id):
306
            raise VirtualMachine.InvalidBackendIdError(str(name))
307
        ns = str(name).lstrip(backend_prefix_id)
308
        if not ns.isdigit():
309
            raise VirtualMachine.InvalidBackendIdError(str(name))
310
        return int(ns)
311

    
312
    def __init__(self, *args, **kw):
313
        """Initialize state for just created VM instances."""
314
        super(VirtualMachine, self).__init__(*args, **kw)
315
        # Before this instance gets save()d
316
        if not self.pk: 
317
            self._action = None
318
            self._operstate = "BUILD"
319
            self.updated = datetime.datetime.now()
320
            self._backendjobid = None
321
            self._backendjobstatus = None
322
            self._backendopcode = None
323
            self._backendlogmsg = None
324

    
325
    def process_backend_msg(self, jobid, opcode, status, logmsg):
326
        """Process a job progress notification from the backend.
327

328
        Process an incoming message from the backend (currently Ganeti).
329
        Job notifications with a terminating status (sucess, error, or canceled),
330
        also update the operating state of the VM.
331

332
        """
333
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
334
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
335
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
336

    
337
        self._backendjobid = jobid
338
        self._backendjobstatus = status
339
        self._backendopcode = opcode
340
        self._backendlogmsg = logmsg
341

    
342
        # Notifications of success change the operating state
343
        if status == 'success':
344
            self._operstate = VirtualMachine.OPER_STATE_FROM_OPCODE[opcode]
345
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
346
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
347
            self._operstate = 'ERROR'
348
        # Any other notification of failure leaves the operating state unchanged
349

    
350
        # FIXME: Should be implemented in a pre-save signal handler.
351
        self.updated = datetime.datetime.now()
352
        self.save()
353

    
354
    def start_action(self, action):
355
        """Update the state of a VM when a new action is initiated."""
356
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
357
            raise VirtualMachine.InvalidActionError(action)
358

    
359
        self._action = action
360
        self._backendjobid = None
361
        self._backendopcode = None
362
        self._backendlogmsg = None
363
        self.updated = datetime.datetime.now()
364
        self.save()
365

    
366
    # FIXME: Perhaps move somewhere else, outside the model?
367
    def _get_rsapi_state(self):
368
        try:
369
            return VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
370
        except KeyError:
371
            return "UNKNOWN"
372

    
373
    rsapi_state = property(_get_rsapi_state)
374

    
375
    def _get_backend_id(self):
376
        """Returns the backend id for this VM by prepending backend-prefix."""
377
        return '%s%s' % (backend_prefix_id, str(self.id))
378

    
379
    backend_id = property(_get_backend_id)
380

    
381
    class Meta:
382
        verbose_name = u'Virtual machine instance'
383
        get_latest_by = 'created'
384
    
385
    def __unicode__(self):
386
        return self.name
387

    
388

    
389
class VirtualMachineGroup(models.Model):
390
    "Groups of VM's for SynnefoUsers"
391
    name = models.CharField(max_length=255)
392
    owner = models.ForeignKey(SynnefoUser)
393
    machines = models.ManyToManyField(VirtualMachine)
394
    created = models.DateTimeField(help_text=_("Group creation date"), default=datetime.datetime.now())
395

    
396
    class Meta:
397
        verbose_name = u'Virtual Machine Group'
398
        verbose_name_plural = 'Virtual Machine Groups'
399
        ordering = ['name']
400
    
401
    def __unicode__(self):
402
        return self.name
403

    
404

    
405
class VirtualMachineMetadata(models.Model):
406
    meta_key = models.CharField(max_length=50)
407
    meta_value = models.CharField(max_length=500)
408
    vm = models.ForeignKey(VirtualMachine)
409
    
410
    class Meta:
411
        verbose_name = u'Key-value pair of metadata for a VM.'
412
    
413
    def __unicode__(self):
414
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
415

    
416

    
417
class AccountingLog(models.Model):
418
    vm = models.ForeignKey(VirtualMachine)
419
    date = models.DateTimeField()
420
    state = models.CharField(choices=VirtualMachine.OPER_STATES, max_length=30)
421
    
422
    class Meta:
423
        verbose_name = u'Accounting log'
424

    
425
    def __unicode__(self):
426
        return u'%s - %s - %s' % (self.vm.name, str(self.date), self.state)
427
    
428
    @staticmethod   
429
    def get_log_entries(vm_obj, date_from):
430
        """Returns log entries for the specified vm after a date"""
431
        entries = AccountingLog.objects.filter(vm=vm_obj).filter(date__gte=date_from).order_by('-date')
432
    
433
        return entries
434

    
435

    
436
class Disk(models.Model):
437
    name = models.CharField(max_length=255)
438
    created = models.DateTimeField('Disk creation date', default=datetime.datetime.now())
439
    size = models.PositiveIntegerField('Disk size in GBs')
440
    vm = models.ForeignKey(VirtualMachine, blank=True, null=True)
441
    owner = models.ForeignKey(SynnefoUser, blank=True, null=True)  
442
    
443
    class Meta:
444
        verbose_name = u'Disk instance'
445

    
446
    def __unicode__(self):
447
        return self.name