Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 4d5d0b9c

History | View | Annotate | Download (25.8 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 VirtualMachine(models.Model):
188
    # The list of possible actions for a VM
189
    ACTIONS = (
190
       ('CREATE', 'Create VM'),
191
       ('START', 'Start VM'),
192
       ('STOP', 'Shutdown VM'),
193
       ('SUSPEND', 'Admin Suspend VM'),
194
       ('REBOOT', 'Reboot VM'),
195
       ('DESTROY', 'Destroy VM')
196
    )
197

    
198
    # The internal operating state of a VM
199
    OPER_STATES = (
200
        ('BUILD', 'Queued for creation'),
201
        ('ERROR', 'Creation failed'),
202
        ('STOPPED', 'Stopped'),
203
        ('STARTED', 'Started'),
204
        ('DESTROYED', 'Destroyed')
205
    )
206

    
207
    # The list of possible operations on the backend
208
    BACKEND_OPCODES = (
209
        ('OP_INSTANCE_CREATE', 'Create Instance'),
210
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
211
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
212
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
213
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
214

    
215
        # These are listed here for completeness,
216
        # and are ignored for the time being
217
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
218
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
219
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
220
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
221
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
222
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
223
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
224
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
225
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
226
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
227
    )
228

    
229
    # The operating state of a VM,
230
    # upon the successful completion of a backend operation.
231
    # IMPORTANT: Make sure all keys have a corresponding
232
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
233
    OPER_STATE_FROM_OPCODE = {
234
        'OP_INSTANCE_CREATE': 'STARTED',
235
        'OP_INSTANCE_REMOVE': 'DESTROYED',
236
        'OP_INSTANCE_STARTUP': 'STARTED',
237
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
238
        'OP_INSTANCE_REBOOT': 'STARTED',
239
        'OP_INSTANCE_SET_PARAMS': None,
240
        'OP_INSTANCE_QUERY_DATA': None,
241
        'OP_INSTANCE_REINSTALL': None,
242
        'OP_INSTANCE_ACTIVATE_DISKS': None,
243
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
244
        'OP_INSTANCE_REPLACE_DISKS': None,
245
        'OP_INSTANCE_MIGRATE': None,
246
        'OP_INSTANCE_CONSOLE': None,
247
        'OP_INSTANCE_RECREATE_DISKS': None,
248
        'OP_INSTANCE_FAILOVER': None
249
    }
250

    
251
    # This dictionary contains the correspondence between
252
    # internal operating states and Server States as defined
253
    # by the Rackspace API.
254
    RSAPI_STATE_FROM_OPER_STATE = {
255
        "BUILD": "BUILD",
256
        "ERROR": "ERROR",
257
        "STOPPED": "STOPPED",
258
        "STARTED": "ACTIVE",
259
        "DESTROYED": "DELETED"
260
    }
261

    
262
    name = models.CharField('Virtual Machine Name', max_length=255)
263
    userid = models.CharField('User ID of the owner', max_length=100,
264
                              db_index=True)
265
    backend = models.ForeignKey(Backend, null=True,
266
                                related_name="virtual_machines",)
267
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
268
    created = models.DateTimeField(auto_now_add=True)
269
    updated = models.DateTimeField(auto_now=True)
270
    imageid = models.CharField(max_length=100, null=False)
271
    hostid = models.CharField(max_length=100)
272
    flavor = models.ForeignKey(Flavor)
273
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
274
    suspended = models.BooleanField('Administratively Suspended',
275
                                    default=False)
276

    
277
    # VM State
278
    # The following fields are volatile data, in the sense
279
    # that they need not be persistent in the DB, but rather
280
    # get generated at runtime by quering Ganeti and applying
281
    # updates received from Ganeti.
282

    
283
    # In the future they could be moved to a separate caching layer
284
    # and removed from the database.
285
    # [vkoukis] after discussion with [faidon].
286
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
287
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
288
    backendjobid = models.PositiveIntegerField(null=True)
289
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
290
                                     null=True)
291
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
292
                                        max_length=30, null=True)
293
    backendlogmsg = models.TextField(null=True)
294
    buildpercentage = models.IntegerField(default=0)
295
    backendtime = models.DateTimeField(default=datetime.datetime.min)
296

    
297
    objects = ForUpdateManager()
298

    
299
    def get_client(self):
300
        if self.backend:
301
            return self.backend.get_client()
302
        else:
303
            raise ServiceUnavailable
304

    
305
    def get_last_diagnostic(self, **filters):
306
        try:
307
            return self.diagnostics.filter()[0]
308
        except IndexError:
309
            return None
310

    
311
    @staticmethod
312
    def put_client(client):
313
            put_rapi_client(client)
314

    
315
    # Error classes
316
    class InvalidBackendIdError(Exception):
317
        def __init__(self, value):
318
            self.value = value
319

    
320
        def __str__(self):
321
            return repr(self.value)
322

    
323
    class InvalidBackendMsgError(Exception):
324
        def __init__(self, opcode, status):
325
            self.opcode = opcode
326
            self.status = status
327

    
328
        def __str__(self):
329
            return repr('<opcode: %s, status: %s>' % (self.opcode,
330
                        self.status))
331

    
332
    class InvalidActionError(Exception):
333
        def __init__(self, action):
334
            self._action = action
335

    
336
        def __str__(self):
337
            return repr(str(self._action))
338

    
339
    class DeletedError(Exception):
340
        pass
341

    
342
    class BuildingError(Exception):
343
        pass
344

    
345
    def __init__(self, *args, **kw):
346
        """Initialize state for just created VM instances."""
347
        super(VirtualMachine, self).__init__(*args, **kw)
348
        # This gets called BEFORE an instance gets save()d for
349
        # the first time.
350
        if not self.pk:
351
            self.action = None
352
            self.backendjobid = None
353
            self.backendjobstatus = None
354
            self.backendopcode = None
355
            self.backendlogmsg = None
356
            self.operstate = 'BUILD'
357

    
358
    def save(self, *args, **kwargs):
359
        # Store hash for first time saved vm
360
        if (self.id is None or self.backend_hash == '') and self.backend:
361
            self.backend_hash = self.backend.hash
362
        super(VirtualMachine, self).save(*args, **kwargs)
363

    
364
    @property
365
    def backend_vm_id(self):
366
        """Returns the backend id for this VM by prepending backend-prefix."""
367
        if not self.id:
368
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
369
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
370

    
371
    class Meta:
372
        verbose_name = u'Virtual machine instance'
373
        get_latest_by = 'created'
374

    
375
    def __unicode__(self):
376
        return str(self.id)
377

    
378

    
379
class VirtualMachineMetadata(models.Model):
380
    meta_key = models.CharField(max_length=50)
381
    meta_value = models.CharField(max_length=500)
382
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
383

    
384
    class Meta:
385
        unique_together = (('meta_key', 'vm'),)
386
        verbose_name = u'Key-value pair of metadata for a VM.'
387

    
388
    def __unicode__(self):
389
        return u'%s: %s' % (self.meta_key, self.meta_value)
390

    
391

    
392
class Network(models.Model):
393
    OPER_STATES = (
394
        ('PENDING', 'Pending'),
395
        ('ACTIVE', 'Active'),
396
        ('DELETED', 'Deleted'),
397
        ('ERROR', 'Error')
398
    )
399

    
400
    ACTIONS = (
401
       ('CREATE', 'Create Network'),
402
       ('DESTROY', 'Destroy Network'),
403
    )
404

    
405
    RSAPI_STATE_FROM_OPER_STATE = {
406
        'PENDING': 'PENDING',
407
        'ACTIVE': 'ACTIVE',
408
        'DELETED': 'DELETED',
409
        'ERROR': 'ERROR'
410
    }
411

    
412
    NETWORK_TYPES = (
413
        ('PUBLIC_ROUTED', 'Public routed network'),
414
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
415
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
416
        ('CUSTOM_ROUTED', 'Custom routed network'),
417
        ('CUSTOM_BRIDGED', 'Custom bridged network')
418
    )
419

    
420
    name = models.CharField('Network Name', max_length=128)
421
    userid = models.CharField('User ID of the owner', max_length=128,
422
                              null=True, db_index=True)
423
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
424
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
425
    gateway = models.CharField('Gateway', max_length=32, null=True)
426
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
427
    dhcp = models.BooleanField('DHCP', default=True)
428
    type = models.CharField(choices=NETWORK_TYPES, max_length=50,
429
                            default='PRIVATE_PHYSICAL_VLAN')
430
    link = models.CharField('Network Link', max_length=128, null=True)
431
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
432
    public = models.BooleanField(default=False, db_index=True)
433
    created = models.DateTimeField(auto_now_add=True)
434
    updated = models.DateTimeField(auto_now=True)
435
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
436
    state = models.CharField(choices=OPER_STATES, max_length=32,
437
                             default='PENDING')
438
    machines = models.ManyToManyField(VirtualMachine,
439
                                      through='NetworkInterface')
440
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
441
                              default=None)
442

    
443
    pool = models.OneToOneField('IPPoolTable', related_name='network',
444
                default=lambda: IPPoolTable.objects.create(available_map='',
445
                                                           reserved_map='',
446
                                                           size=0),
447
                null=True)
448

    
449
    objects = ForUpdateManager()
450

    
451
    def __unicode__(self):
452
        return str(self.id)
453

    
454
    class InvalidBackendIdError(Exception):
455
        def __init__(self, value):
456
            self.value = value
457

    
458
        def __str__(self):
459
            return repr(self.value)
460

    
461
    class InvalidBackendMsgError(Exception):
462
        def __init__(self, opcode, status):
463
            self.opcode = opcode
464
            self.status = status
465

    
466
        def __str__(self):
467
            return repr('<opcode: %s, status: %s>' % (self.opcode,
468
                    self.status))
469

    
470
    class InvalidActionError(Exception):
471
        def __init__(self, action):
472
            self._action = action
473

    
474
        def __str__(self):
475
            return repr(str(self._action))
476

    
477
    class DeletedError(Exception):
478
        pass
479

    
480
    @property
481
    def backend_id(self):
482
        """Return the backend id by prepending backend-prefix."""
483
        if not self.id:
484
            raise Network.InvalidBackendIdError("self.id is None")
485
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
486

    
487
    @property
488
    def backend_tag(self):
489
        """Return the network tag to be used in backend
490

491
        """
492
        return getattr(snf_settings, self.type + '_TAGS')
493

    
494
    def create_backend_network(self, backend=None):
495
        """Create corresponding BackendNetwork entries."""
496

    
497
        backends = [backend] if backend else Backend.objects.all()
498
        for backend in backends:
499
            BackendNetwork.objects.create(backend=backend, network=self)
500

    
501
    def get_pool(self):
502
        if not self.pool_id:
503
            self.pool = IPPoolTable.objects.create(available_map='',
504
                                                   reserved_map='',
505
                                                   size=0)
506
            self.save()
507
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id).pool
508

    
509
    def reserve_address(self, address):
510
        pool = self.get_pool()
511
        pool.reserve(address)
512
        pool.save()
513

    
514
    def release_address(self, address):
515
        pool = self.get_pool()
516
        pool.put(address)
517
        pool.save()
518

    
519

    
520
class BackendNetwork(models.Model):
521
    OPER_STATES = (
522
        ('PENDING', 'Pending'),
523
        ('ACTIVE', 'Active'),
524
        ('DELETED', 'Deleted'),
525
        ('ERROR', 'Error')
526
    )
527

    
528
    # The list of possible operations on the backend
529
    BACKEND_OPCODES = (
530
        ('OP_NETWORK_ADD', 'Create Network'),
531
        ('OP_NETWORK_CONNECT', 'Activate Network'),
532
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
533
        ('OP_NETWORK_REMOVE', 'Remove Network'),
534
        # These are listed here for completeness,
535
        # and are ignored for the time being
536
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
537
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
538
    )
539

    
540
    # The operating state of a Netowork,
541
    # upon the successful completion of a backend operation.
542
    # IMPORTANT: Make sure all keys have a corresponding
543
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
544
    OPER_STATE_FROM_OPCODE = {
545
        'OP_NETWORK_ADD': 'PENDING',
546
        'OP_NETWORK_CONNECT': 'ACTIVE',
547
        'OP_NETWORK_DISCONNECT': 'PENDING',
548
        'OP_NETWORK_REMOVE': 'DELETED',
549
        'OP_NETWORK_SET_PARAMS': None,
550
        'OP_NETWORK_QUERY_DATA': None
551
    }
552

    
553
    network = models.ForeignKey(Network, related_name='backend_networks')
554
    backend = models.ForeignKey(Backend, related_name='networks')
555
    created = models.DateTimeField(auto_now_add=True)
556
    updated = models.DateTimeField(auto_now=True)
557
    deleted = models.BooleanField('Deleted', default=False)
558
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
559
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
560
                                 default='PENDING')
561
    backendjobid = models.PositiveIntegerField(null=True)
562
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
563
                                     null=True)
564
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
565
                                        max_length=30, null=True)
566
    backendlogmsg = models.TextField(null=True)
567
    backendtime = models.DateTimeField(null=False,
568
                                       default=datetime.datetime.min)
569

    
570
    class Meta:
571
        # Ensure one entry for each network in each backend
572
        unique_together = (("network", "backend"))
573

    
574
    def __init__(self, *args, **kwargs):
575
        """Initialize state for just created BackendNetwork instances."""
576
        super(BackendNetwork, self).__init__(*args, **kwargs)
577
        if not self.mac_prefix:
578
            # Generate the MAC prefix of the BackendNetwork, by combining
579
            # the Network prefix with the index of the Backend
580
            net_prefix = self.network.mac_prefix
581
            backend_suffix = hex(self.backend.index).replace('0x', '')
582
            mac_prefix = net_prefix + backend_suffix
583
            try:
584
                utils.validate_mac(mac_prefix + ":00:00:00")
585
            except utils.InvalidMacAddress:
586
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
587
                                               mac_prefix)
588
            self.mac_prefix = mac_prefix
589

    
590

    
591
class NetworkInterface(models.Model):
592
    FIREWALL_PROFILES = (
593
        ('ENABLED', 'Enabled'),
594
        ('DISABLED', 'Disabled'),
595
        ('PROTECTED', 'Protected')
596
    )
597

    
598
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
599
    network = models.ForeignKey(Network, related_name='nics')
600
    created = models.DateTimeField(auto_now_add=True)
601
    updated = models.DateTimeField(auto_now=True)
602
    index = models.IntegerField(null=False)
603
    mac = models.CharField(max_length=32, null=False, unique=True)
604
    ipv4 = models.CharField(max_length=15, null=True)
605
    ipv6 = models.CharField(max_length=100, null=True)
606
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
607
                                        max_length=30, null=True)
608
    dirty = models.BooleanField(default=False)
609

    
610
    def __unicode__(self):
611
        return '%s@%s' % (self.machine.name, self.network.name)
612

    
613

    
614
class PoolTable(models.Model):
615
    available_map = models.TextField(default="", null=False)
616
    reserved_map = models.TextField(default="", null=False)
617
    size = models.IntegerField(null=False)
618

    
619
    # Optional Fields
620
    base = models.CharField(null=True, max_length=32)
621
    offset = models.IntegerField(null=True)
622

    
623
    objects = ForUpdateManager()
624

    
625
    class Meta:
626
        abstract = True
627

    
628
    @classmethod
629
    def get_pool(cls):
630
        try:
631
            pool_row = cls.objects.select_for_update().all()[0]
632
            return pool_row.pool
633
        except IndexError:
634
            raise pools.EmptyPool
635

    
636
    @property
637
    def pool(self):
638
        return self.manager(self)
639

    
640

    
641
class BridgePoolTable(PoolTable):
642
    manager = pools.BridgePool
643

    
644

    
645
class MacPrefixPoolTable(PoolTable):
646
    manager = pools.MacPrefixPool
647

    
648

    
649
class IPPoolTable(PoolTable):
650
    manager = pools.IPPool
651

    
652

    
653
@contextmanager
654
def pooled_rapi_client(obj):
655
        if isinstance(obj, VirtualMachine):
656
            backend = obj.backend
657
        else:
658
            backend = obj
659

    
660
        if backend.offline:
661
            raise ServiceUnavailable
662

    
663
        b = backend
664
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
665
                                 b.username, b.password)
666
        try:
667
            yield client
668
        finally:
669
            put_rapi_client(client)
670

    
671

    
672
class VirtualMachineDiagnosticManager(models.Manager):
673
    """
674
    Custom manager for :class:`VirtualMachineDiagnostic` model.
675
    """
676

    
677
    # diagnostic creation helpers
678
    def create_for_vm(self, vm, level, message, **kwargs):
679
        attrs = {'machine': vm, 'level': level, 'message': message}
680
        attrs.update(kwargs)
681
        # update instance updated time
682
        self.create(**attrs)
683
        vm.save()
684

    
685
    def create_error(self, vm, **kwargs):
686
        self.create_for_vm(vm, 'ERROR', **kwargs)
687

    
688
    def create_debug(self, vm, **kwargs):
689
        self.create_for_vm(vm, 'DEBUG', **kwargs)
690

    
691
    def since(self, vm, created_since, **kwargs):
692
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
693
                **kwargs)
694

    
695

    
696
class VirtualMachineDiagnostic(models.Model):
697
    """
698
    Model to store backend information messages that relate to the state of
699
    the virtual machine.
700
    """
701

    
702
    TYPES = (
703
        ('ERROR', 'Error'),
704
        ('WARNING', 'Warning'),
705
        ('INFO', 'Info'),
706
        ('DEBUG', 'Debug'),
707
    )
708

    
709
    objects = VirtualMachineDiagnosticManager()
710

    
711
    created = models.DateTimeField(auto_now_add=True)
712
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
713
    level = models.CharField(max_length=20, choices=TYPES)
714
    source = models.CharField(max_length=100)
715
    source_date = models.DateTimeField(null=True)
716
    message = models.CharField(max_length=255)
717
    details = models.TextField(null=True)
718

    
719
    class Meta:
720
        ordering = ['-created']
721