Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 3846dfd0

History | View | Annotate | Download (13.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
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
        Returns amount of credits remaining. Negative if the user surpassed his limit."""
42
        td = end - start
43
        sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
44
        
45
        total_hours = float(sec) / float(60.0*60.0)
46
        total_cost = float(cost)*total_hours
47
        
48
        self.credit = self.credit - round(total_cost)
49
        
50
        if self.credit < 0:
51
            self.violations = self.violations + 1
52
        else:
53
            self.violations = 0
54
                
55
        return self.credit
56
    
57
    def allocate_credits(self):
58
        """Allocate credits. Add monthly rate to user credit reserve."""
59
        self.credit = self.credit + self.monthly_rate
60
        
61
        # ensure that the user has not more credits than his quota
62
        if self.credit > self.quota:
63
            self.credit = self.quota
64

    
65

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

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

    
84
    class Meta:
85
        verbose_name = u'Image'
86

    
87
    def __unicode__(self):
88
        return u'%s' % (self.name)
89

    
90
    def get_vmid(self):
91
        """Returns first Virtual Machine's id, if any"""
92
        if self.virtualmachine_set.all():
93
            return self.virtualmachine_set.all()[0].id
94
        else:
95
            return ''
96

    
97
    vm_id = property(get_vmid)
98

    
99

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

    
111

    
112
class UserLimit(models.Model):
113
    user = models.ForeignKey(SynnefoUser)
114
    limit = models.ForeignKey(Limit)
115
    value = models.IntegerField()
116
    
117
    class Meta:
118
        unique_together = ('user', 'limit')
119
        verbose_name = u'Enforced limit for user'
120
    
121
    def __unicode__(self):
122
        return u'Limit %s for user %s: %d' % (self.limit, self.user, self.value)
123

    
124

    
125
class Flavor(models.Model):
126
    cpu = models.IntegerField(default=0)
127
    ram = models.IntegerField(default=0)
128
    disk = models.IntegerField(default=0)
129
    
130
    class Meta:
131
        verbose_name = u'Virtual machine flavor'
132
            
133
    def _get_name(self):
134
        """Returns flavor name"""
135
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
136

    
137
    def _get_cost_inactive(self):
138
        self._update_costs()
139
        return self._cost_inactive
140

    
141
    def _get_cost_active(self):
142
        self._update_costs()
143
        return self._cost_active
144
    
145
    def _update_costs(self):
146
        # if _cost_active is not defined, then define it!
147
        if '_cost_active' not in dir(self):
148
            fch_list = FlavorCostHistory.objects.filter(flavor=self).order_by('-effective_from')
149
            if len(fch_list) > 0:
150
                fch = fch_list[0]
151
                self._cost_active = fch.cost_active
152
                self._cost_inactive = fch.cost_inactive
153
            else:
154
                self._cost_active = 0
155
                self._cost_inactive = 0
156

    
157
    name = property(_get_name)
158
    cost_active = property(_get_cost_active)
159
    cost_inactive = property(_get_cost_inactive)
160

    
161
    def __unicode__(self):
162
        return self.name
163
    
164
    def get_price_list(self):
165
        fch_list = FlavorCostHistory.objects.get(flavor=self).order_by('effective_from')
166
        
167
        return fch_list            
168

    
169

    
170
class FlavorCostHistory(models.Model):
171
    cost_active = models.PositiveIntegerField()
172
    cost_inactive = models.PositiveIntegerField()
173
    effective_from = models.DateField()
174
    flavor = models.ForeignKey(Flavor)
175
    
176
    class Meta:
177
        verbose_name = u'Pricing history for flavors'
178
    
179
    def __unicode__(self):
180
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (self.cost_active, self.cost_inactive, flavor.name, self.effective_from)
181
        
182
    @staticmethod
183
    def find_cost(fch_list, dat):
184
        rdate = fch_list[0]
185

    
186
        for fc in fch_list:
187
            if dat > fc.effective_from:
188
                rdate = fc
189
        
190
        return rdate
191

    
192

    
193
class VirtualMachine(models.Model):
194
    ACTIONS = (
195
       ('CREATE', 'Create VM'),
196
       ('START', 'Start VM'),
197
       ('STOP', 'Shutdown VM'),
198
       ('SUSPEND', 'Admin Suspend VM'),
199
       ('REBOOT', 'Reboot VM'),
200
       ('DESTROY', 'Destroy VM')
201
    )
202

    
203
    OPER_STATES = (
204
        ('BUILD', 'Queued for creation'),
205
        ('ERROR', 'Creation failed'),
206
        ('STOPPED', 'Stopped'),
207
        ('STARTED', 'Started'),
208
        ('DESTROYED', 'Destroyed')
209
    )
210

    
211
    BACKEND_OPCODES = (
212
        ('OP_INSTANCE_CREATE', 'Create Instance'),
213
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
214
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
215
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
216
        ('OP_INSTANCE_REBOOT', 'Reboot Instance')
217
    )
218

    
219
    BACKEND_STATUSES = (
220
        ('queued', 'request queued'),
221
        ('waiting', 'request waiting for locks'),
222
        ('canceling', 'request being canceled'),
223
        ('running', 'request running'),
224
        ('canceled', 'request canceled'),
225
        ('success', 'request completed successfully'),
226
        ('error', 'request returned error')
227
    )
228

    
229
    # The operating state of a VM,
230
    # upon the successful completion of a backend operation.
231
    OPER_STATE_FROM_OPCODE = {
232
        'OP_INSTANCE_CREATE': 'STARTED',
233
        'OP_INSTANCE_REMOVE': 'DESTROYED',
234
        'OP_INSTANCE_STARTUP': 'STARTED',
235
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
236
        'OP_INSTANCE_REBOOT': 'STARTED'
237
    }
238

    
239
    RSAPI_STATE_FROM_OPER_STATE = {
240
        "BUILD": "BUILD",
241
        "ERROR": "ERROR",
242
        "STOPPED": "STOPPED",
243
        "STARTED": "ACTIVE",
244
        "DESTROYED": "DELETED"
245
    }
246

    
247
    name = models.CharField(max_length=255)
248
    created = models.DateTimeField(help_text=_('VM creation date'), default=datetime.datetime.now())
249
    charged = models.DateTimeField()
250
    # Use string reference to avoid circular ForeignKey def.
251
    # FIXME: "sourceimage" works, "image" causes validation errors. See "related_name" in the Django docs.
252
    sourceimage = models.ForeignKey(Image) 
253
    hostid = models.CharField(max_length=100)
254
    description = models.TextField(help_text=_('description'))
255
    ipfour = models.IPAddressField()
256
    ipsix = models.CharField(max_length=100)
257
    owner = models.ForeignKey(SynnefoUser)
258
    flavor = models.ForeignKey(Flavor)
259
    suspended = models.BooleanField('Administratively Suspended')
260

    
261
    # VM State [volatile data]
262
    updated = models.DateTimeField(null=True)
263
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
264
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
265
    _backendjobid = models.PositiveIntegerField(null=True)
266
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
267
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
268
    _backendlogmsg = models.TextField(null=True)
269

    
270
    # Error classes
271
    class InvalidBackendIdError(Exception):
272
         def __init__(self, value):
273
            self.value = value
274
         def __str__(self):
275
            return repr(self.value)
276

    
277
    class InvalidBackendMsgError(Exception):
278
         def __init__(self, opcode, status):
279
            self.opcode = opcode
280
            self.status = status
281
         def __str__(self):
282
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
283

    
284
    class InvalidActionError(Exception):
285
         def __init__(self, action):
286
            self.__action = action
287
         def __str__(self):
288
            return repr(str(self._action))
289

    
290
    @staticmethod
291
    def id_from_instance_name(name):
292
        """Returns VirtualMachine's Django id, given a ganeti machine name.
293

294
        Strips the ganeti prefix atm. Needs a better name!
295
        
296
        """
297
        if not str(name).startswith(backend_prefix_id):
298
            raise VirtualMachine.InvalidBackendIdError(str(name))
299
        ns = str(name).lstrip(backend_prefix_id)
300
        if not ns.isdigit():
301
            raise VirtualMachine.InvalidBackendIdError(str(name))
302
        return int(ns)
303

    
304
    def __init__(self, *args, **kw):
305
        """Initialize state for just created VM instances."""
306
        super(VirtualMachine, self).__init__(*args, **kw)
307
        # Before this instance gets save()d
308
        if not self.pk: 
309
            self._action = None
310
            self._operstate = "BUILD"
311
            self.updated = datetime.datetime.now()
312
            self._backendjobid = None
313
            self._backendjobstatus = None
314
            self._backendopcode = None
315
            self._backendlogmsg = None
316

    
317
    def process_backend_msg(self, jobid, opcode, status, logmsg):
318
        """Process a job progress notification from the backend.
319

320
        Process an incoming message from the backend (currently Ganeti).
321
        Job notifications with a terminating status (sucess, error, or canceled),
322
        also update the operating state of the VM.
323

324
        """
325
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
326
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
327
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
328

    
329
        self._backendjobid = jobid
330
        self._backendjobstatus = status
331
        self._backendopcode = opcode
332
        self._backendlogmsg = logmsg
333

    
334
        # Notifications of success change the operating state
335
        if status == 'success':
336
            self._operstate = VirtualMachine.OPER_STATE_FROM_OPCODE[opcode]
337
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
338
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
339
            self._operstate = 'ERROR'
340
        # Any other notification of failure leaves the operating state unchanged
341

    
342
        # FIXME: Should be implemented in a pre-save signal handler.
343
        self.updated = datetime.datetime.now()
344
        self.save()
345

    
346
    def start_action(self, action):
347
        """Update the state of a VM when a new action is initiated."""
348
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
349
            raise VirtualMachine.InvalidActionError(action)
350

    
351
        self._action = action
352
        self._backendjobid = None
353
        self._backendopcode = None
354
        self._backendlogmsg = None
355
        self.updated = datetime.datetime.now()
356
        self.save()
357

    
358
    # FIXME: Perhaps move somewhere else, outside the model?
359
    def _get_rsapi_state(self):
360
        try:
361
            return VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
362
        except KeyError:
363
            return "UNKNOWN"
364

    
365
    rsapi_state = property(_get_rsapi_state)
366

    
367
    def _get_backend_id(self):
368
        """Returns the backend id for this VM by prepending backend-prefix."""
369
        return '%s%s' % (backend_prefix_id, str(self.id))
370

    
371
    backend_id = property(_get_backend_id)
372

    
373
    class Meta:
374
        verbose_name = u'Virtual machine instance'
375
        get_latest_by = 'created'
376
    
377
    def __unicode__(self):
378
        return self.name
379

    
380

    
381
class VirtualMachineGroup(models.Model):
382
    "Groups of VM's for SynnefoUsers"
383
    name = models.CharField(max_length=255)
384
    owner = models.ForeignKey(SynnefoUser)
385
    machines = models.ManyToManyField(VirtualMachine)
386
    created = models.DateTimeField(help_text=_("Group creation date"), default=datetime.datetime.now())
387

    
388
    class Meta:
389
        verbose_name = u'Virtual Machine Group'
390
        verbose_name_plural = 'Virtual Machine Groups'
391
        ordering = ['name']
392
    
393
    def __unicode__(self):
394
        return self.name
395

    
396

    
397
class VirtualMachineMetadata(models.Model):
398
    meta_key = models.CharField(max_length=50)
399
    meta_value = models.CharField(max_length=500)
400
    vm = models.ForeignKey(VirtualMachine)
401
    
402
    class Meta:
403
        verbose_name = u'Key-value pair of metadata for a VM.'
404
    
405
    def __unicode__(self):
406
        return u'%s, %s for %s' % (self.meta_key, self.meta_value, self.vm.name)
407

    
408

    
409
class AccountingLog(models.Model):
410
    vm = models.ForeignKey(VirtualMachine)
411
    date = models.DateTimeField()
412
    state = models.CharField(choices=VirtualMachine.OPER_STATES, max_length=30)
413
    
414
    class Meta:
415
        verbose_name = u'Accounting log'
416

    
417
    def __unicode__(self):
418
        return u'%s %s %s' % (self.vm.name, self.date, self.state)
419