Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.4 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
                default=lambda: IPPoolTable.objects.create(available_map='',
431
                                                           reserved_map='',
432
                                                           size=0),
433
                null=True)
434

    
435
    objects = ForUpdateManager()
436

    
437
    def __unicode__(self):
438
        return str(self.id)
439

    
440
    class InvalidBackendIdError(Exception):
441
        def __init__(self, value):
442
            self.value = value
443

    
444
        def __str__(self):
445
            return repr(self.value)
446

    
447
    class InvalidBackendMsgError(Exception):
448
        def __init__(self, opcode, status):
449
            self.opcode = opcode
450
            self.status = status
451

    
452
        def __str__(self):
453
            return repr('<opcode: %s, status: %s>' % (self.opcode,
454
                    self.status))
455

    
456
    class InvalidActionError(Exception):
457
        def __init__(self, action):
458
            self._action = action
459

    
460
        def __str__(self):
461
            return repr(str(self._action))
462

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

    
470
    @property
471
    def backend_tag(self):
472
        """Return the network tag to be used in backend
473

474
        """
475
        return getattr(snf_settings, self.type + '_TAGS')
476

    
477
    @transaction.commit_on_success
478
    def update_state(self):
479
        """Update state of the Network.
480

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

486
        """
487

    
488
        old_state = self.state
489

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

    
496
        all_equal = len(set(backend_states)) <= 1
497
        self.state = all_equal and backend_states[0] or 'PENDING'
498

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

    
509
            if self.link and self.type == 'PRIVATE_PHYSICAL_VLAN':
510
                bridge_pool = BridgePoolTable.get_pool()
511
                bridge_pool.put(self.link)
512
                bridge_pool.save()
513

    
514
        self.save()
515

    
516
    def create_backend_network(self, backend=None):
517
        """Create corresponding BackendNetwork entries."""
518

    
519
        backends = [backend] if backend else Backend.objects.all()
520
        for backend in backends:
521
            BackendNetwork.objects.create(backend=backend, network=self)
522

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

    
531
    def reserve_address(self, address):
532
        pool = self.get_pool()
533
        pool.reserve(address)
534
        pool.save()
535

    
536
    def release_address(self, address):
537
        pool = self.get_pool()
538
        pool.put(address)
539
        pool.save()
540

    
541

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

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

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

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

    
592
    class Meta:
593
        # Ensure one entry for each network in each backend
594
        unique_together = (("network", "backend"))
595

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

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

    
616
    def delete(self, *args, **kwargs):
617
        super(BackendNetwork, self).delete(*args, **kwargs)
618
        self.network.update_state()
619

    
620

    
621
class NetworkInterface(models.Model):
622
    FIREWALL_PROFILES = (
623
        ('ENABLED', 'Enabled'),
624
        ('DISABLED', 'Disabled'),
625
        ('PROTECTED', 'Protected')
626
    )
627

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

    
640
    def __unicode__(self):
641
        return '%s@%s' % (self.machine.name, self.network.name)
642

    
643

    
644
class PoolTable(models.Model):
645
    available_map = models.TextField(default="", null=False)
646
    reserved_map = models.TextField(default="", null=False)
647
    size = models.IntegerField(null=False)
648

    
649
    # Optional Fields
650
    base = models.CharField(null=True, max_length=32)
651
    offset = models.IntegerField(null=True)
652

    
653
    objects = ForUpdateManager()
654

    
655
    class Meta:
656
        abstract = True
657

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

    
666
    @property
667
    def pool(self):
668
        return self.manager(self)
669

    
670

    
671
class BridgePoolTable(PoolTable):
672
    manager = pools.BridgePool
673

    
674

    
675
class MacPrefixPoolTable(PoolTable):
676
    manager = pools.MacPrefixPool
677

    
678

    
679
class IPPoolTable(PoolTable):
680
    manager = pools.IPPool
681

    
682

    
683
@contextmanager
684
def pooled_rapi_client(obj):
685
        if isinstance(obj, VirtualMachine):
686
            backend = obj.backend
687
        else:
688
            backend = obj
689

    
690
        if backend.offline:
691
            raise ServiceUnavailable
692

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