Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.1 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 self.name
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.username,
121
                               self.password)
122

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

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

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

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

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

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

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

    
168

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

    
180

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

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

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

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

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

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

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

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

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

    
290
    def get_client(self):
291
        if self.backend:
292
            return self.backend.get_rapi_client()
293
        else:
294
            raise ServiceUnavailable
295

    
296
    @staticmethod
297
    def put_client(client):
298
            put_rapi_client(client)
299

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

    
305
        def __str__(self):
306
            return repr(self.value)
307

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

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

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

    
321
        def __str__(self):
322
            return repr(str(self._action))
323

    
324
    class DeletedError(Exception):
325
        pass
326

    
327
    class BuildingError(Exception):
328
        pass
329

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

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

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

    
356
    class Meta:
357
        verbose_name = u'Virtual machine instance'
358
        get_latest_by = 'created'
359

    
360
    def __unicode__(self):
361
        return self.name + "(id=" + str(self.id) + ")"
362

    
363

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

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

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

    
376

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

    
385
    ACTIONS = (
386
       ('CREATE', 'Create Network'),
387
       ('DESTROY', 'Destroy Network'),
388
    )
389

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

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

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

    
427
    pool = models.OneToOneField('IPPoolTable', related_name='network',
428
                                null=True)
429

    
430
    objects = ForUpdateManager()
431

    
432
    class InvalidBackendIdError(Exception):
433
        def __init__(self, value):
434
            self.value = value
435

    
436
        def __str__(self):
437
            return repr(self.value)
438

    
439
    class InvalidBackendMsgError(Exception):
440
        def __init__(self, opcode, status):
441
            self.opcode = opcode
442
            self.status = status
443

    
444
        def __str__(self):
445
            return repr('<opcode: %s, status: %s>' % (self.opcode,
446
                    self.status))
447

    
448
    class InvalidActionError(Exception):
449
        def __init__(self, action):
450
            self._action = action
451

    
452
        def __str__(self):
453
            return repr(str(self._action))
454

    
455
    @property
456
    def backend_id(self):
457
        """Return the backend id by prepending backend-prefix."""
458
        if not self.id:
459
            raise Network.InvalidBackendIdError("self.id is None")
460
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
461

    
462
    @property
463
    def backend_tag(self):
464
        """Return the network tag to be used in backend
465

466
        """
467
        return getattr(snf_settings, self.type + '_TAGS')
468

    
469
    def __unicode__(self):
470
        return self.name + "(id=" + str(self.id) + ")"
471

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

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

481
        """
482

    
483
        old_state = self.state
484

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

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

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

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

    
509
        self.save()
510

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

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

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

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

    
531
    def release_address(self, address):
532
        pool = self.get_pool()
533
        pool.put(address)
534
        pool.save()
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
    def save(self, *args, **kwargs):
608
        super(BackendNetwork, self).save(*args, **kwargs)
609
        self.network.update_state()
610

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

    
615

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

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

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

    
638

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

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

    
648
    objects = ForUpdateManager()
649

    
650
    class Meta:
651
        abstract = True
652

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

    
661
    @property
662
    def pool(self):
663
        return self.manager(self)
664

    
665

    
666
class BridgePoolTable(PoolTable):
667
    manager = pools.BridgePool
668

    
669

    
670
class MacPrefixPoolTable(PoolTable):
671
    manager = pools.MacPrefixPool
672

    
673

    
674
class IPPoolTable(PoolTable):
675
    manager = pools.IPPool
676

    
677

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

    
685
        if backend.offline:
686
            raise ServiceUnavailable
687

    
688
        b = backend
689
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
690
                                 b.username, b.password)
691
        yield client
692
        put_rapi_client(client)