Statistics
| Branch: | Tag: | Revision:

root / db / models.py @ 00d83c42

History | View | Annotate | Download (12.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
    
31
    class Meta:
32
        verbose_name = u'Synnefo User'
33
    
34
    def __unicode__(self):
35
        return self.name
36
    
37
    def _total_hours(self, start, end):
38
        """Calculate duration in hours"""
39
        td = end - start
40
        sec = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
41
        
42
        return float(sec) / float(60.0*60.0)
43
    
44
    def charge_credits(self, cost, start, end):
45
        """Reduce user credits for specified duration. 
46
        Returns amount of credits remaining. Negative if the user surpassed his limit."""
47
        total_cost = float(cost)*self._total_hours(start, end)
48
        
49
        self.credit = self.credit - round(total_cost)
50
        rcredit = self.credit
51
                
52
        if self.credit < 0:
53
            self.credit = 0
54
        
55
        return rcredit 
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
class UserLimit(models.Model):
100
    user = models.ForeignKey(SynnefoUser)
101
    limit = models.ForeignKey(Limit)
102
    value = models.IntegerField()
103
    
104
    class Meta:
105
        unique_together = ('user', 'limit')
106
        verbose_name = u'Enforced limit for user'
107
    
108
    def __unicode__(self):
109
        return u'Limit %s for user %s: %d' % (self.limit, self.user, self.value)
110

    
111

    
112
class Flavor(models.Model):
113
    cpu = models.IntegerField(default=0)
114
    ram = models.IntegerField(default=0)
115
    disk = models.IntegerField(default=0)
116
    
117
    class Meta:
118
        verbose_name = u'Virtual machine flavor'
119
            
120
    def _get_name(self):
121
        """Returns flavor name"""
122
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
123

    
124
    def _get_cost_inactive(self):
125
        self._update_costs()
126
        return self._cost_inactive
127

    
128
    def _get_cost_active(self):
129
        self._update_costs()
130
        return self._cost_active
131
    
132
    def _update_costs(self):
133
        # if _cost
134
        if '_cost_active' not in dir(self):
135
            fch = FlavorCostHistory.objects.filter(flavor=self).order_by('-effective_from')[0]
136
            self._cost_active = fch.cost_active
137
            self._cost_inactive = fch.cost_inactive
138

    
139
    name = property(_get_name)
140
    cost_active = property(_get_cost_active)
141
    cost_inactive = property(_get_cost_inactive)
142

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

    
146

    
147
class FlavorCostHistory(models.Model):
148
    cost_active = models.PositiveIntegerField()
149
    cost_inactive = models.PositiveIntegerField()
150
    effective_from = models.DateField()
151
    flavor = models.ForeignKey(Flavor)
152
    
153
    class Meta:
154
        verbose_name = u'Pricing history for flavors'
155
    
156
    def __unicode__(self):
157
        return u'Costs (up, down)=(%d, %d) for %s since %s' % (cost_active, cost_inactive, flavor.name, effective_from)
158

    
159

    
160
class VirtualMachine(models.Model):
161
    ACTIONS = (
162
       ('CREATE', 'Create VM'),
163
       ('START', 'Start VM'),
164
       ('STOP', 'Shutdown VM'),
165
       ('SUSPEND', 'Admin Suspend VM'),
166
       ('REBOOT', 'Reboot VM'),
167
       ('DESTROY', 'Destroy VM')
168
    )
169

    
170
    OPER_STATES = (
171
        ('BUILD', 'Queued for creation'),
172
        ('ERROR', 'Creation failed'),
173
        ('STOPPED', 'Stopped'),
174
        ('STARTED', 'Started'),
175
        ('DESTROYED', 'Destroyed')
176
    )
177

    
178
    BACKEND_OPCODES = (
179
        ('OP_INSTANCE_CREATE', 'Create Instance'),
180
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
181
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
182
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
183
        ('OP_INSTANCE_REBOOT', 'Reboot Instance')
184
    )
185

    
186
    BACKEND_STATUSES = (
187
        ('queued', 'request queued'),
188
        ('waiting', 'request waiting for locks'),
189
        ('canceling', 'request being canceled'),
190
        ('running', 'request running'),
191
        ('canceled', 'request canceled'),
192
        ('success', 'request completed successfully'),
193
        ('error', 'request returned error')
194
    )
195

    
196
    # The operating state of a VM,
197
    # upon the successful completion of a backend operation.
198
    OPER_STATE_FROM_OPCODE = {
199
        'OP_INSTANCE_CREATE': 'STARTED',
200
        'OP_INSTANCE_REMOVE': 'DESTROYED',
201
        'OP_INSTANCE_STARTUP': 'STARTED',
202
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
203
        'OP_INSTANCE_REBOOT': 'STARTED'
204
    }
205

    
206
    RSAPI_STATE_FROM_OPER_STATE = {
207
        "BUILD": "BUILD",
208
        "ERROR": "ERROR",
209
        "STOPPED": "STOPPED",
210
        "STARTED": "ACTIVE",
211
        "DESTROYED": "DELETED"
212
    }
213

    
214
    name = models.CharField(max_length=255)
215
    created = models.DateTimeField(help_text=_('VM creation date'), default=datetime.datetime.now)
216
    charged = models.DateTimeField()
217
    # Use string reference to avoid circular ForeignKey def.
218
    # FIXME: "sourceimage" works, "image" causes validation errors. See "related_name" in the Django docs.
219
    sourceimage = models.ForeignKey(Image) 
220
    hostid = models.CharField(max_length=100)
221
    description = models.TextField(help_text=_('description'))
222
    ipfour = models.IPAddressField()
223
    ipsix = models.CharField(max_length=100)
224
    owner = models.ForeignKey(SynnefoUser)
225
    flavor = models.ForeignKey(Flavor)
226
    suspended = models.BooleanField('Administratively Suspended')
227

    
228
    # VM State [volatile data]
229
    updated = models.DateTimeField(null=True)
230
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
231
    _operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
232
    _backendjobid = models.PositiveIntegerField(null=True)
233
    _backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30, null=True)
234
    _backendjobstatus = models.CharField(choices=BACKEND_STATUSES, max_length=30, null=True)
235
    _backendlogmsg = models.TextField(null=True)
236

    
237
    # Error classes
238
    class InvalidBackendIdError(Exception):
239
         def __init__(self, value):
240
            self.value = value
241
         def __str__(self):
242
            return repr(self.value)
243

    
244
    class InvalidBackendMsgError(Exception):
245
         def __init__(self, opcode, status):
246
            self.opcode = opcode
247
            self.status = status
248
         def __str__(self):
249
            return repr("<opcode: %s, status: %s>" % (str(self.opcode), str(self.status)))
250

    
251
    class InvalidActionError(Exception):
252
         def __init__(self, action):
253
            self.__action = action
254
         def __str__(self):
255
            return repr(str(self._action))
256

    
257

    
258
    @staticmethod
259
    def id_from_instance_name(name):
260
        """Returns VirtualMachine's Django id, given a ganeti machine name.
261

262
        Strips the ganeti prefix atm. Needs a better name!
263
        
264
        """
265
        if not str(name).startswith(backend_prefix_id):
266
            raise VirtualMachine.InvalidBackendIdError(str(name))
267
        ns = str(name).lstrip(backend_prefix_id)
268
        if not ns.isdigit():
269
            raise VirtualMachine.InvalidBackendIdError(str(name))
270
        return int(ns)
271

    
272
    def __init__(self, *args, **kw): 
273
        """Initialize state for just created VM instances."""
274
        super(VirtualMachine, self).__init__(*args, **kw)
275
        # Before this instance gets save()d
276
        if not self.pk: 
277
            self._action = None
278
            self._operstate = "BUILD"
279
            self.updated = datetime.datetime.now()
280
            self._backendjobid = None
281
            self._backendjobstatus = None
282
            self._backendopcode = None
283
            self._backendlogmsg = None
284

    
285
    def process_backend_msg(self, jobid, opcode, status, logmsg):
286
        """Process a job progress notification from the backend.
287

288
        Process an incoming message from the backend (currently Ganeti).
289
        Job notifications with a terminating status (sucess, error, or canceled),
290
        also update the operating state of the VM.
291

292
        """
293
        if (opcode not in [x[0] for x in VirtualMachine.BACKEND_OPCODES] or
294
           status not in [x[0] for x in VirtualMachine.BACKEND_STATUSES]):
295
            raise VirtualMachine.InvalidBackendMsgError(opcode, status)
296

    
297
        self._backendjobid = jobid
298
        self._backendjobstatus = status
299
        self._backendopcode = opcode
300
        self._backendlogmsg = logmsg
301

    
302
        # Notifications of success change the operating state
303
        if status == 'success':
304
            self._operstate = VirtualMachine.OPER_STATE_FROM_OPCODE[opcode]
305
        # Special cases OP_INSTANCE_CREATE fails --> ERROR
306
        if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
307
            self._operstate = 'ERROR'
308
        # Any other notification of failure leaves the operating state unchanged
309

    
310
        # FIXME: Should be implemented in a pre-save signal handler.
311
        self.updated = datetime.datetime.now()
312
        self.save()
313

    
314
    def start_action(self, action):
315
        """Update the state of a VM when a new action is initiated."""
316
        if not action in [x[0] for x in VirtualMachine.ACTIONS]:
317
            raise VirtualMachine.InvalidActionError(action)
318

    
319
        self._action = action
320
        self._backendjobid = None
321
        self._backendopcode = None
322
        self._backendlogmsg = None
323
        self.updated = datetime.datetime.now()
324
        self.save()
325

    
326
    # FIXME: Perhaps move somewhere else, outside the model?
327
    def _get_rsapi_state(self):
328
        try:
329
            return VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[self._operstate]
330
        except KeyError:
331
            return "UNKNOWN"
332

    
333
    rsapi_state = property(_get_rsapi_state)
334

    
335
    def _get_backend_id(self):
336
        """Returns the backend id for this VM by prepending backend-prefix."""
337
        return '%s%s' % (backend_prefix_id, str(self.id))
338

    
339
    backend_id = property(_get_backend_id)
340

    
341
    class Meta:
342
        verbose_name = u'Virtual machine instance'
343
        get_latest_by = 'created'
344
    
345
    def __unicode__(self):
346
        return self.name
347

    
348

    
349
class VirtualMachineGroup(models.Model):
350
    "Groups of VM's for SynnefoUsers"
351
    name = models.CharField(max_length=255)
352
    owner = models.ForeignKey(SynnefoUser)
353
    machines = models.ManyToManyField(VirtualMachine)
354
    created = models.DateTimeField(help_text=_("Group creation date"), default=datetime.datetime.now)
355

    
356
    class Meta:
357
        verbose_name = u'Virtual Machine Group'
358
        verbose_name_plural = 'Virtual Machine Groups'
359
        ordering = ['name']
360
    
361
    def __unicode__(self):
362
        return self.name
363

    
364

    
365
class VirtualMachineMetadata(models.Model):
366
    meta_key = models.CharField(max_length=50)
367
    meta_value = models.CharField(max_length=500)
368
    vm = models.ForeignKey(VirtualMachine)
369
    
370
    class Meta:
371
        verbose_name = u'Key-value pair of metadata for a VM.'
372
    
373
    def __unicode__(self):
374
        return u'%s, %s for %s' % (self.key, self.value, self.vm.name)
375

    
376

    
377
class AccountingLog(models.Model):
378
    vm = models.ForeignKey(VirtualMachine)
379
    date = models.DateTimeField()
380
    state = models.CharField(choices=VirtualMachine.OPER_STATES, max_length=30)
381
    
382
    class Meta:
383
        verbose_name = u'Accounting log'
384

    
385
    def __unicode__(self):
386
        return u'%s %s %s' % (self.vm.name, self.date, self.state)
387