Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 004967e4

History | View | Annotate | Download (26.6 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import datetime
31

    
32
from django.conf import settings
33
from django.db import models
34
from django.db import IntegrityError
35

    
36
import utils
37
from contextlib import contextmanager
38
from hashlib import sha1
39
from synnefo.api.faults import ServiceUnavailable
40
from synnefo import settings as snf_settings
41
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
42

    
43
from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
44
from synnefo.db import pools
45

    
46
from synnefo.logic.rapi_pool import (get_rapi_client,
47
                                     put_rapi_client)
48

    
49
import logging
50
log = logging.getLogger(__name__)
51

    
52

    
53
class Flavor(models.Model):
54
    cpu = models.IntegerField('Number of CPUs', default=0)
55
    ram = models.IntegerField('RAM size in MiB', default=0)
56
    disk = models.IntegerField('Disk size in GiB', default=0)
57
    disk_template = models.CharField('Disk template', max_length=32,
58
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
59
    deleted = models.BooleanField('Deleted', default=False)
60

    
61
    class Meta:
62
        verbose_name = u'Virtual machine flavor'
63
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
64

    
65
    @property
66
    def name(self):
67
        """Returns flavor name (generated)"""
68
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
69

    
70
    def __unicode__(self):
71
        return str(self.id)
72

    
73

    
74
class Backend(models.Model):
75
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
76
    port = models.PositiveIntegerField('Port', default=5080)
77
    username = models.CharField('Username', max_length=64, blank=True,
78
                                null=True)
79
    password_hash = models.CharField('Password', max_length=128, blank=True,
80
                                null=True)
81
    # Sha1 is up to 40 characters long
82
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
83
    # Unique index of the Backend, used for the mac-prefixes of the
84
    # BackendNetworks
85
    index = models.PositiveIntegerField('Index', null=False, unique=True,
86
                                        default=0)
87
    drained = models.BooleanField('Drained', default=False, null=False)
88
    offline = models.BooleanField('Offline', default=False, null=False)
89
    # Last refresh of backend resources
90
    updated = models.DateTimeField(auto_now_add=True)
91
    # Backend resources
92
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
93
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
94
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
95
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
96
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
97
                                            null=False)
98
    ctotal = models.PositiveIntegerField('Total number of logical processors',
99
                                         default=0, null=False)
100
    # Custom object manager to protect from cascade delete
101
    objects = ProtectedDeleteManager()
102

    
103
    class Meta:
104
        verbose_name = u'Backend'
105
        ordering = ["clustername"]
106

    
107
    def __unicode__(self):
108
        return self.clustername + "(id=" + str(self.id) + ")"
109

    
110
    @property
111
    def backend_id(self):
112
        return self.id
113

    
114
    def get_client(self):
115
        """Get or create a client. """
116
        if self.offline:
117
            raise ServiceUnavailable
118
        return get_rapi_client(self.id, self.hash,
119
                               self.clustername,
120
                               self.port,
121
                               self.username,
122
                               self.password)
123

    
124
    @staticmethod
125
    def put_client(client):
126
            put_rapi_client(client)
127

    
128
    def create_hash(self):
129
        """Create a hash for this backend. """
130
        return sha1('%s%s%s%s' % \
131
                (self.clustername, self.port, self.username, self.password)) \
132
                .hexdigest()
133

    
134
    @property
135
    def password(self):
136
        return decrypt_db_charfield(self.password_hash)
137

    
138
    @password.setter
139
    def password(self, value):
140
        self.password_hash = encrypt_db_charfield(value)
141

    
142
    def save(self, *args, **kwargs):
143
        # Create a new hash each time a Backend is saved
144
        old_hash = self.hash
145
        self.hash = self.create_hash()
146
        super(Backend, self).save(*args, **kwargs)
147
        if self.hash != old_hash:
148
            # Populate the new hash to the new instances
149
            self.virtual_machines.filter(deleted=False).update(backend_hash=self.hash)
150

    
151
    def delete(self, *args, **kwargs):
152
        # Integrity Error if non-deleted VMs are associated with Backend
153
        if self.virtual_machines.filter(deleted=False).count():
154
            raise IntegrityError("Non-deleted virtual machines are associated "
155
                                 "with backend: %s" % self)
156
        else:
157
            # ON_DELETE = SET NULL
158
            self.virtual_machines.all().backend = None
159
            # Remove BackendNetworks of this Backend.
160
            # Do not use networks.all().delete(), since delete() method of
161
            # BackendNetwork will not be called!
162
            for net in self.networks.all():
163
                net.delete()
164
            super(Backend, self).delete(*args, **kwargs)
165

    
166
    def __init__(self, *args, **kwargs):
167
        super(Backend, self).__init__(*args, **kwargs)
168
        if not self.pk:
169
            # Generate a unique index for the Backend
170
            indexes = Backend.objects.all().values_list('index', flat=True)
171
            first_free = [x for x in xrange(0, 16) if x not in indexes][0]
172
            self.index = first_free
173

    
174

    
175
# A backend job may be in one of the following possible states
176
BACKEND_STATUSES = (
177
    ('queued', 'request queued'),
178
    ('waiting', 'request waiting for locks'),
179
    ('canceling', 'request being canceled'),
180
    ('running', 'request running'),
181
    ('canceled', 'request canceled'),
182
    ('success', 'request completed successfully'),
183
    ('error', 'request returned error')
184
)
185

    
186

    
187
class QuotaHolderSerial(models.Model):
188
    serial = models.BigIntegerField(null=False, primary_key=True, db_index=True)
189
    pending = models.BooleanField(default=True, db_index=True)
190
    accepted = models.BooleanField(default=False)
191
    rejected = models.BooleanField(default=False)
192

    
193
    class Meta:
194
        verbose_name = u'Quota Serial'
195
        ordering = ["serial"]
196

    
197
    def save(self, *args, **kwargs):
198
        self.pending = not (self.accepted or self.rejected)
199
        super(QuotaHolderSerial, self).save(*args, **kwargs)
200

    
201

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

    
213
    # The internal operating state of a VM
214
    OPER_STATES = (
215
        ('BUILD', 'Queued for creation'),
216
        ('ERROR', 'Creation failed'),
217
        ('STOPPED', 'Stopped'),
218
        ('STARTED', 'Started'),
219
        ('DESTROYED', 'Destroyed')
220
    )
221

    
222
    # The list of possible operations on the backend
223
    BACKEND_OPCODES = (
224
        ('OP_INSTANCE_CREATE', 'Create Instance'),
225
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
226
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
227
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
228
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
229

    
230
        # These are listed here for completeness,
231
        # and are ignored for the time being
232
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
233
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
234
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
235
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
236
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
237
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
238
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
239
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
240
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
241
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
242
    )
243

    
244
    # The operating state of a VM,
245
    # upon the successful completion of a backend operation.
246
    # IMPORTANT: Make sure all keys have a corresponding
247
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
248
    OPER_STATE_FROM_OPCODE = {
249
        'OP_INSTANCE_CREATE': 'STARTED',
250
        'OP_INSTANCE_REMOVE': 'DESTROYED',
251
        'OP_INSTANCE_STARTUP': 'STARTED',
252
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
253
        'OP_INSTANCE_REBOOT': 'STARTED',
254
        'OP_INSTANCE_SET_PARAMS': None,
255
        'OP_INSTANCE_QUERY_DATA': None,
256
        'OP_INSTANCE_REINSTALL': None,
257
        'OP_INSTANCE_ACTIVATE_DISKS': None,
258
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
259
        'OP_INSTANCE_REPLACE_DISKS': None,
260
        'OP_INSTANCE_MIGRATE': None,
261
        'OP_INSTANCE_CONSOLE': None,
262
        'OP_INSTANCE_RECREATE_DISKS': None,
263
        'OP_INSTANCE_FAILOVER': None
264
    }
265

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

    
277
    name = models.CharField('Virtual Machine Name', max_length=255)
278
    userid = models.CharField('User ID of the owner', max_length=100,
279
                              db_index=True)
280
    backend = models.ForeignKey(Backend, null=True,
281
                                related_name="virtual_machines",)
282
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
283
    created = models.DateTimeField(auto_now_add=True)
284
    updated = models.DateTimeField(auto_now=True)
285
    imageid = models.CharField(max_length=100, null=False)
286
    hostid = models.CharField(max_length=100)
287
    flavor = models.ForeignKey(Flavor)
288
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
289
    suspended = models.BooleanField('Administratively Suspended',
290
                                    default=False)
291
    serial = models.ForeignKey(QuotaHolderSerial,
292
                              related_name='virtual_machine', null=True)
293

    
294
    # VM State
295
    # The following fields are volatile data, in the sense
296
    # that they need not be persistent in the DB, but rather
297
    # get generated at runtime by quering Ganeti and applying
298
    # updates received from Ganeti.
299

    
300
    # In the future they could be moved to a separate caching layer
301
    # and removed from the database.
302
    # [vkoukis] after discussion with [faidon].
303
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
304
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
305
    backendjobid = models.PositiveIntegerField(null=True)
306
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
307
                                     null=True)
308
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
309
                                        max_length=30, null=True)
310
    backendlogmsg = models.TextField(null=True)
311
    buildpercentage = models.IntegerField(default=0)
312
    backendtime = models.DateTimeField(default=datetime.datetime.min)
313

    
314
    objects = ForUpdateManager()
315

    
316
    def get_client(self):
317
        if self.backend:
318
            return self.backend.get_client()
319
        else:
320
            raise ServiceUnavailable
321

    
322
    def get_last_diagnostic(self, **filters):
323
        try:
324
            return self.diagnostics.filter()[0]
325
        except IndexError:
326
            return None
327

    
328
    @staticmethod
329
    def put_client(client):
330
            put_rapi_client(client)
331

    
332
    def __init__(self, *args, **kw):
333
        """Initialize state for just created VM instances."""
334
        super(VirtualMachine, self).__init__(*args, **kw)
335
        # This gets called BEFORE an instance gets save()d for
336
        # the first time.
337
        if not self.pk:
338
            self.action = None
339
            self.backendjobid = None
340
            self.backendjobstatus = None
341
            self.backendopcode = None
342
            self.backendlogmsg = None
343
            self.operstate = 'BUILD'
344

    
345
    def save(self, *args, **kwargs):
346
        # Store hash for first time saved vm
347
        if (self.id is None or self.backend_hash == '') and self.backend:
348
            self.backend_hash = self.backend.hash
349
        super(VirtualMachine, self).save(*args, **kwargs)
350

    
351
    @property
352
    def backend_vm_id(self):
353
        """Returns the backend id for this VM by prepending backend-prefix."""
354
        if not self.id:
355
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
356
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
357

    
358
    class Meta:
359
        verbose_name = u'Virtual machine instance'
360
        get_latest_by = 'created'
361

    
362
    def __unicode__(self):
363
        return str(self.id)
364

    
365
    # Error classes
366
    class InvalidBackendIdError(Exception):
367
        def __init__(self, value):
368
            self.value = value
369

    
370
        def __str__(self):
371
            return repr(self.value)
372

    
373
    class InvalidBackendMsgError(Exception):
374
        def __init__(self, opcode, status):
375
            self.opcode = opcode
376
            self.status = status
377

    
378
        def __str__(self):
379
            return repr('<opcode: %s, status: %s>' % (self.opcode,
380
                        self.status))
381

    
382
    class InvalidActionError(Exception):
383
        def __init__(self, action):
384
            self._action = action
385

    
386
        def __str__(self):
387
            return repr(str(self._action))
388

    
389
    class DeletedError(Exception):
390
        pass
391

    
392
    class BuildingError(Exception):
393
        pass
394

    
395

    
396
class VirtualMachineMetadata(models.Model):
397
    meta_key = models.CharField(max_length=50)
398
    meta_value = models.CharField(max_length=500)
399
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
400

    
401
    class Meta:
402
        unique_together = (('meta_key', 'vm'),)
403
        verbose_name = u'Key-value pair of metadata for a VM.'
404

    
405
    def __unicode__(self):
406
        return u'%s: %s' % (self.meta_key, self.meta_value)
407

    
408

    
409
class Network(models.Model):
410
    OPER_STATES = (
411
        ('PENDING', 'Pending'),
412
        ('ACTIVE', 'Active'),
413
        ('DELETED', 'Deleted'),
414
        ('ERROR', 'Error')
415
    )
416

    
417
    ACTIONS = (
418
       ('CREATE', 'Create Network'),
419
       ('DESTROY', 'Destroy Network'),
420
    )
421

    
422
    RSAPI_STATE_FROM_OPER_STATE = {
423
        'PENDING': 'PENDING',
424
        'ACTIVE': 'ACTIVE',
425
        'DELETED': 'DELETED',
426
        'ERROR': 'ERROR'
427
    }
428

    
429
    NETWORK_TYPES = (
430
        ('PUBLIC_ROUTED', 'Public routed network'),
431
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
432
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
433
        ('CUSTOM_ROUTED', 'Custom routed network'),
434
        ('CUSTOM_BRIDGED', 'Custom bridged network')
435
    )
436

    
437
    name = models.CharField('Network Name', max_length=128)
438
    userid = models.CharField('User ID of the owner', max_length=128,
439
                              null=True, db_index=True)
440
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
441
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
442
    gateway = models.CharField('Gateway', max_length=32, null=True)
443
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
444
    dhcp = models.BooleanField('DHCP', default=True)
445
    type = models.CharField(choices=NETWORK_TYPES, max_length=50,
446
                            default='PRIVATE_PHYSICAL_VLAN')
447
    link = models.CharField('Network Link', max_length=128, null=True)
448
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
449
    public = models.BooleanField(default=False, db_index=True)
450
    created = models.DateTimeField(auto_now_add=True)
451
    updated = models.DateTimeField(auto_now=True)
452
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
453
    state = models.CharField(choices=OPER_STATES, max_length=32,
454
                             default='PENDING')
455
    machines = models.ManyToManyField(VirtualMachine,
456
                                      through='NetworkInterface')
457
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
458
                              default=None)
459

    
460
    pool = models.OneToOneField('IPPoolTable', related_name='network',
461
                default=lambda: IPPoolTable.objects.create(available_map='',
462
                                                           reserved_map='',
463
                                                           size=0),
464
                null=True)
465
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
466
                               null=True)
467

    
468
    objects = ForUpdateManager()
469

    
470
    def __unicode__(self):
471
        return str(self.id)
472

    
473
    @property
474
    def backend_id(self):
475
        """Return the backend id by prepending backend-prefix."""
476
        if not self.id:
477
            raise Network.InvalidBackendIdError("self.id is None")
478
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
479

    
480
    @property
481
    def backend_tag(self):
482
        """Return the network tag to be used in backend
483

484
        """
485
        return getattr(snf_settings, self.type + '_TAGS')
486

    
487
    def create_backend_network(self, backend=None):
488
        """Create corresponding BackendNetwork entries."""
489

    
490
        backends = [backend] if backend else Backend.objects.all()
491
        for backend in backends:
492
            BackendNetwork.objects.create(backend=backend, network=self)
493

    
494
    def get_pool(self):
495
        if not self.pool_id:
496
            self.pool = IPPoolTable.objects.create(available_map='',
497
                                                   reserved_map='',
498
                                                   size=0)
499
            self.save()
500
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
501
                                                      .pool
502

    
503
    def reserve_address(self, address):
504
        pool = self.get_pool()
505
        pool.reserve(address)
506
        pool.save()
507

    
508
    def release_address(self, address):
509
        pool = self.get_pool()
510
        pool.put(address)
511
        pool.save()
512

    
513
    class InvalidBackendIdError(Exception):
514
        def __init__(self, value):
515
            self.value = value
516

    
517
        def __str__(self):
518
            return repr(self.value)
519

    
520
    class InvalidBackendMsgError(Exception):
521
        def __init__(self, opcode, status):
522
            self.opcode = opcode
523
            self.status = status
524

    
525
        def __str__(self):
526
            return repr('<opcode: %s, status: %s>' % (self.opcode,
527
                    self.status))
528

    
529
    class InvalidActionError(Exception):
530
        def __init__(self, action):
531
            self._action = action
532

    
533
        def __str__(self):
534
            return repr(str(self._action))
535

    
536

    
537
class BackendNetwork(models.Model):
538
    OPER_STATES = (
539
        ('PENDING', 'Pending'),
540
        ('ACTIVE', 'Active'),
541
        ('DELETED', 'Deleted'),
542
        ('ERROR', 'Error')
543
    )
544

    
545
    # The list of possible operations on the backend
546
    BACKEND_OPCODES = (
547
        ('OP_NETWORK_ADD', 'Create Network'),
548
        ('OP_NETWORK_CONNECT', 'Activate Network'),
549
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
550
        ('OP_NETWORK_REMOVE', 'Remove Network'),
551
        # These are listed here for completeness,
552
        # and are ignored for the time being
553
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
554
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
555
    )
556

    
557
    # The operating state of a Netowork,
558
    # upon the successful completion of a backend operation.
559
    # IMPORTANT: Make sure all keys have a corresponding
560
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
561
    OPER_STATE_FROM_OPCODE = {
562
        'OP_NETWORK_ADD': 'PENDING',
563
        'OP_NETWORK_CONNECT': 'ACTIVE',
564
        'OP_NETWORK_DISCONNECT': 'PENDING',
565
        'OP_NETWORK_REMOVE': 'DELETED',
566
        'OP_NETWORK_SET_PARAMS': None,
567
        'OP_NETWORK_QUERY_DATA': None
568
    }
569

    
570
    network = models.ForeignKey(Network, related_name='backend_networks')
571
    backend = models.ForeignKey(Backend, related_name='networks')
572
    created = models.DateTimeField(auto_now_add=True)
573
    updated = models.DateTimeField(auto_now=True)
574
    deleted = models.BooleanField('Deleted', default=False)
575
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
576
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
577
                                 default='PENDING')
578
    backendjobid = models.PositiveIntegerField(null=True)
579
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
580
                                     null=True)
581
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
582
                                        max_length=30, null=True)
583
    backendlogmsg = models.TextField(null=True)
584
    backendtime = models.DateTimeField(null=False,
585
                                       default=datetime.datetime.min)
586

    
587
    class Meta:
588
        # Ensure one entry for each network in each backend
589
        unique_together = (("network", "backend"))
590

    
591
    def __init__(self, *args, **kwargs):
592
        """Initialize state for just created BackendNetwork instances."""
593
        super(BackendNetwork, self).__init__(*args, **kwargs)
594
        if not self.mac_prefix:
595
            # Generate the MAC prefix of the BackendNetwork, by combining
596
            # the Network prefix with the index of the Backend
597
            net_prefix = self.network.mac_prefix
598
            backend_suffix = hex(self.backend.index).replace('0x', '')
599
            mac_prefix = net_prefix + backend_suffix
600
            try:
601
                utils.validate_mac(mac_prefix + ":00:00:00")
602
            except utils.InvalidMacAddress:
603
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
604
                                               mac_prefix)
605
            self.mac_prefix = mac_prefix
606

    
607

    
608
class NetworkInterface(models.Model):
609
    FIREWALL_PROFILES = (
610
        ('ENABLED', 'Enabled'),
611
        ('DISABLED', 'Disabled'),
612
        ('PROTECTED', 'Protected')
613
    )
614

    
615
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
616
    network = models.ForeignKey(Network, related_name='nics')
617
    created = models.DateTimeField(auto_now_add=True)
618
    updated = models.DateTimeField(auto_now=True)
619
    index = models.IntegerField(null=False)
620
    mac = models.CharField(max_length=32, null=False, unique=True)
621
    ipv4 = models.CharField(max_length=15, null=True)
622
    ipv6 = models.CharField(max_length=100, null=True)
623
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
624
                                        max_length=30, null=True)
625
    dirty = models.BooleanField(default=False)
626

    
627
    def __unicode__(self):
628
        return '%s@%s' % (self.machine.name, self.network.name)
629

    
630

    
631
class PoolTable(models.Model):
632
    available_map = models.TextField(default="", null=False)
633
    reserved_map = models.TextField(default="", null=False)
634
    size = models.IntegerField(null=False)
635

    
636
    # Optional Fields
637
    base = models.CharField(null=True, max_length=32)
638
    offset = models.IntegerField(null=True)
639

    
640
    objects = ForUpdateManager()
641

    
642
    class Meta:
643
        abstract = True
644

    
645
    @classmethod
646
    def get_pool(cls):
647
        try:
648
            pool_row = cls.objects.select_for_update().all()[0]
649
            return pool_row.pool
650
        except IndexError:
651
            raise pools.EmptyPool
652

    
653
    @property
654
    def pool(self):
655
        return self.manager(self)
656

    
657

    
658
class BridgePoolTable(PoolTable):
659
    manager = pools.BridgePool
660

    
661

    
662
class MacPrefixPoolTable(PoolTable):
663
    manager = pools.MacPrefixPool
664

    
665

    
666
class IPPoolTable(PoolTable):
667
    manager = pools.IPPool
668

    
669

    
670
@contextmanager
671
def pooled_rapi_client(obj):
672
        if isinstance(obj, VirtualMachine):
673
            backend = obj.backend
674
        else:
675
            backend = obj
676

    
677
        if backend.offline:
678
            raise ServiceUnavailable
679

    
680
        b = backend
681
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
682
                                 b.username, b.password)
683
        try:
684
            yield client
685
        finally:
686
            put_rapi_client(client)
687

    
688

    
689
class VirtualMachineDiagnosticManager(models.Manager):
690
    """
691
    Custom manager for :class:`VirtualMachineDiagnostic` model.
692
    """
693

    
694
    # diagnostic creation helpers
695
    def create_for_vm(self, vm, level, message, **kwargs):
696
        attrs = {'machine': vm, 'level': level, 'message': message}
697
        attrs.update(kwargs)
698
        # update instance updated time
699
        self.create(**attrs)
700
        vm.save()
701

    
702
    def create_error(self, vm, **kwargs):
703
        self.create_for_vm(vm, 'ERROR', **kwargs)
704

    
705
    def create_debug(self, vm, **kwargs):
706
        self.create_for_vm(vm, 'DEBUG', **kwargs)
707

    
708
    def since(self, vm, created_since, **kwargs):
709
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
710
                **kwargs)
711

    
712

    
713
class VirtualMachineDiagnostic(models.Model):
714
    """
715
    Model to store backend information messages that relate to the state of
716
    the virtual machine.
717
    """
718

    
719
    TYPES = (
720
        ('ERROR', 'Error'),
721
        ('WARNING', 'Warning'),
722
        ('INFO', 'Info'),
723
        ('DEBUG', 'Debug'),
724
    )
725

    
726
    objects = VirtualMachineDiagnosticManager()
727

    
728
    created = models.DateTimeField(auto_now_add=True)
729
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
730
    level = models.CharField(max_length=20, choices=TYPES)
731
    source = models.CharField(max_length=100)
732
    source_date = models.DateTimeField(null=True)
733
    message = models.CharField(max_length=255)
734
    details = models.TextField(null=True)
735

    
736
    class Meta:
737
        ordering = ['-created']