Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 3e5675f0

History | View | Annotate | Download (25.2 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
from django.db import transaction
36

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

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

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

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

    
53

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

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

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

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

    
74

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

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

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

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

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

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

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

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

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

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

    
152
    def delete(self, *args, **kwargs):
153
        # Integrity Error if non-deleted VMs are associated with Backend
154
        if self.virtual_machines.filter(deleted=False).count():
155
            raise IntegrityError("Non-deleted virtual machines are associated "
156
                                 "with backend: %s" % self)
157
        else:
158
            # ON_DELETE = SET NULL
159
            self.virtual_machines.all().backend = None
160
            super(Backend, self).delete(*args, **kwargs)
161

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

    
170

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

    
182

    
183
class VirtualMachine(models.Model):
184
    # The list of possible actions for a VM
185
    ACTIONS = (
186
       ('CREATE', 'Create VM'),
187
       ('START', 'Start VM'),
188
       ('STOP', 'Shutdown VM'),
189
       ('SUSPEND', 'Admin Suspend VM'),
190
       ('REBOOT', 'Reboot VM'),
191
       ('DESTROY', 'Destroy VM')
192
    )
193

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

    
203
    # The list of possible operations on the backend
204
    BACKEND_OPCODES = (
205
        ('OP_INSTANCE_CREATE', 'Create Instance'),
206
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
207
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
208
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
209
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
210

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

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

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

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

    
272
    # VM State
273
    # The following fields are volatile data, in the sense
274
    # that they need not be persistent in the DB, but rather
275
    # get generated at runtime by quering Ganeti and applying
276
    # updates received from Ganeti.
277

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

    
292
    def get_client(self):
293
        if self.backend:
294
            return self.backend.get_client()
295
        else:
296
            raise ServiceUnavailable
297

    
298
    @staticmethod
299
    def put_client(client):
300
            put_rapi_client(client)
301

    
302
    # Error classes
303
    class InvalidBackendIdError(Exception):
304
        def __init__(self, value):
305
            self.value = value
306

    
307
        def __str__(self):
308
            return repr(self.value)
309

    
310
    class InvalidBackendMsgError(Exception):
311
        def __init__(self, opcode, status):
312
            self.opcode = opcode
313
            self.status = status
314

    
315
        def __str__(self):
316
            return repr('<opcode: %s, status: %s>' % (self.opcode,
317
                        self.status))
318

    
319
    class InvalidActionError(Exception):
320
        def __init__(self, action):
321
            self._action = action
322

    
323
        def __str__(self):
324
            return repr(str(self._action))
325

    
326
    class DeletedError(Exception):
327
        pass
328

    
329
    class BuildingError(Exception):
330
        pass
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

    
366
class VirtualMachineMetadata(models.Model):
367
    meta_key = models.CharField(max_length=50)
368
    meta_value = models.CharField(max_length=500)
369
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
370

    
371
    class Meta:
372
        unique_together = (('meta_key', 'vm'),)
373
        verbose_name = u'Key-value pair of metadata for a VM.'
374

    
375
    def __unicode__(self):
376
        return u'%s: %s' % (self.meta_key, self.meta_value)
377

    
378

    
379
class Network(models.Model):
380
    OPER_STATES = (
381
        ('PENDING', 'Pending'),
382
        ('ACTIVE', 'Active'),
383
        ('DELETED', 'Deleted'),
384
        ('ERROR', 'Error')
385
    )
386

    
387
    ACTIONS = (
388
       ('CREATE', 'Create Network'),
389
       ('DESTROY', 'Destroy Network'),
390
    )
391

    
392
    RSAPI_STATE_FROM_OPER_STATE = {
393
        'PENDING': 'PENDING',
394
        'ACTIVE': 'ACTIVE',
395
        'DELETED': 'DELETED',
396
        'ERROR': 'ERROR'
397
    }
398

    
399
    NETWORK_TYPES = (
400
        ('PUBLIC_ROUTED', 'Public routed network'),
401
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
402
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
403
        ('CUSTOM_ROUTED', 'Custom routed network'),
404
        ('CUSTOM_BRIDGED', 'Custom bridged network')
405
    )
406

    
407
    name = models.CharField('Network Name', max_length=128)
408
    userid = models.CharField('User ID of the owner', max_length=128, null=True)
409
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
410
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
411
    gateway = models.CharField('Gateway', max_length=32, null=True)
412
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
413
    dhcp = models.BooleanField('DHCP', default=True)
414
    type = models.CharField(choices=NETWORK_TYPES, max_length=50,
415
                            default='PRIVATE_PHYSICAL_VLAN')
416
    link = models.CharField('Network Link', max_length=128, null=True)
417
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
418
    public = models.BooleanField(default=False)
419
    created = models.DateTimeField(auto_now_add=True)
420
    updated = models.DateTimeField(auto_now=True)
421
    deleted = models.BooleanField('Deleted', default=False)
422
    state = models.CharField(choices=OPER_STATES, max_length=32,
423
                             default='PENDING')
424
    machines = models.ManyToManyField(VirtualMachine,
425
                                      through='NetworkInterface')
426
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
427
                              default=None)
428

    
429
    pool = models.OneToOneField('IPPoolTable', related_name='network',
430
                                null=True)
431

    
432
    objects = ForUpdateManager()
433

    
434
    def __unicode__(self):
435
        return str(self.id)
436

    
437
    class InvalidBackendIdError(Exception):
438
        def __init__(self, value):
439
            self.value = value
440

    
441
        def __str__(self):
442
            return repr(self.value)
443

    
444
    class InvalidBackendMsgError(Exception):
445
        def __init__(self, opcode, status):
446
            self.opcode = opcode
447
            self.status = status
448

    
449
        def __str__(self):
450
            return repr('<opcode: %s, status: %s>' % (self.opcode,
451
                    self.status))
452

    
453
    class InvalidActionError(Exception):
454
        def __init__(self, action):
455
            self._action = action
456

    
457
        def __str__(self):
458
            return repr(str(self._action))
459

    
460
    @property
461
    def backend_id(self):
462
        """Return the backend id by prepending backend-prefix."""
463
        if not self.id:
464
            raise Network.InvalidBackendIdError("self.id is None")
465
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
466

    
467
    @property
468
    def backend_tag(self):
469
        """Return the network tag to be used in backend
470

471
        """
472
        return getattr(snf_settings, self.type + '_TAGS')
473

    
474
    @transaction.commit_on_success
475
    def update_state(self):
476
        """Update state of the Network.
477

478
        Update the state of the Network depending on the related
479
        backend_networks. When backend networks do not have the same operstate,
480
        the Network's state is PENDING. Otherwise it is the same with
481
        the BackendNetworks operstate.
482

483
        """
484

    
485
        old_state = self.state
486

    
487
        backend_states = [s.operstate for s in self.backend_networks.all()]
488
        if not backend_states:
489
            self.state = 'PENDING'
490
            self.save()
491
            return
492

    
493
        all_equal = len(set(backend_states)) <= 1
494
        self.state = all_equal and backend_states[0] or 'PENDING'
495

    
496
        # Release the resources on the deletion of the Network
497
        if old_state != 'DELETED' and self.state == 'DELETED':
498
            log.info("Network %r deleted. Releasing link %r mac_prefix %r",
499
                     self.id, self.mac_prefix, self.link)
500
            self.deleted = True
501
            if self.mac_prefix and self.type == 'PRIVATE_MAC_FILTERED':
502
                mac_pool = MacPrefixPoolTable.get_pool()
503
                mac_pool.put(self.mac_prefix)
504
                mac_pool.save()
505

    
506
            if self.link and self.type == 'PRIVATE_VLAN':
507
                bridge_pool = BridgePoolTable.get_pool()
508
                bridge_pool.put(self.link)
509
                bridge_pool.save()
510

    
511
        self.save()
512

    
513
    def create_backend_network(self, backend=None):
514
        """Create corresponding BackendNetwork entries."""
515

    
516
        backends = [backend] if backend else Backend.objects.all()
517
        for backend in backends:
518
            BackendNetwork.objects.create(backend=backend, network=self)
519

    
520
    def get_pool(self):
521
        if not self.pool_id:
522
            self.pool = IPPoolTable.objects.create(available_map='',
523
                                                   reserved_map='',
524
                                                   size=0)
525
            self.save()
526
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id).pool
527

    
528
    def reserve_address(self, address):
529
        pool = self.get_pool()
530
        pool.reserve(address)
531
        pool.save()
532

    
533
    def release_address(self, address):
534
        pool = self.get_pool()
535
        pool.put(address)
536
        pool.save()
537

    
538

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

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

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

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

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

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

    
609
    def save(self, *args, **kwargs):
610
        super(BackendNetwork, self).save(*args, **kwargs)
611
        self.network.update_state()
612

    
613
    def delete(self, *args, **kwargs):
614
        super(BackendNetwork, self).delete(*args, **kwargs)
615
        self.network.update_state()
616

    
617

    
618
class NetworkInterface(models.Model):
619
    FIREWALL_PROFILES = (
620
        ('ENABLED', 'Enabled'),
621
        ('DISABLED', 'Disabled'),
622
        ('PROTECTED', 'Protected')
623
    )
624

    
625
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
626
    network = models.ForeignKey(Network, related_name='nics')
627
    created = models.DateTimeField(auto_now_add=True)
628
    updated = models.DateTimeField(auto_now=True)
629
    index = models.IntegerField(null=False)
630
    mac = models.CharField(max_length=32, null=False, unique=True)
631
    ipv4 = models.CharField(max_length=15, null=True)
632
    ipv6 = models.CharField(max_length=100, null=True)
633
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
634
                                        max_length=30, null=True)
635
    dirty = models.BooleanField(default=False)
636

    
637
    def __unicode__(self):
638
        return '%s@%s' % (self.machine.name, self.network.name)
639

    
640

    
641
class PoolTable(models.Model):
642
    available_map = models.TextField(default="", null=False)
643
    reserved_map = models.TextField(default="", null=False)
644
    size = models.IntegerField(null=False)
645

    
646
    # Optional Fields
647
    base = models.CharField(null=True, max_length=32)
648
    offset = models.IntegerField(null=True)
649

    
650
    objects = ForUpdateManager()
651

    
652
    class Meta:
653
        abstract = True
654

    
655
    @classmethod
656
    def get_pool(cls):
657
        try:
658
            pool_row = cls.objects.select_for_update().all()[0]
659
            return pool_row.pool
660
        except IndexError:
661
            raise pools.EmptyPool
662

    
663
    @property
664
    def pool(self):
665
        return self.manager(self)
666

    
667

    
668
class BridgePoolTable(PoolTable):
669
    manager = pools.BridgePool
670

    
671

    
672
class MacPrefixPoolTable(PoolTable):
673
    manager = pools.MacPrefixPool
674

    
675

    
676
class IPPoolTable(PoolTable):
677
    manager = pools.IPPool
678

    
679

    
680
@contextmanager
681
def pooled_rapi_client(obj):
682
        if isinstance(obj, VirtualMachine):
683
            backend = obj.backend
684
        else:
685
            backend = obj
686

    
687
        if backend.offline:
688
            raise ServiceUnavailable
689

    
690
        b = backend
691
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
692
                                 b.username, b.password)
693
        try:
694
            yield client
695
        finally:
696
            put_rapi_client(client)