Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.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
            # Remove BackendNetworks of this Backend.
161
            # Do not use networks.all().delete(), since delete() method of
162
            # BackendNetwork will not be called!
163
            for net in self.networks.all():
164
                net.delete()
165
            super(Backend, self).delete(*args, **kwargs)
166

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

    
175

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

    
187

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

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

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

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

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

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

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

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

    
448
    objects = ForUpdateManager()
449

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

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

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

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

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

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

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

    
476
    class DeletedError(Exception):
477
        pass
478

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

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

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

    
493
    @transaction.commit_on_success
494
    def update_state(self):
495
        """Update state of the Network.
496

497
        Update the state of the Network depending on the related
498
        backend_networks. When backend networks do not have the same operstate,
499
        the Network's state is PENDING. Otherwise it is the same with
500
        the BackendNetworks operstate.
501

502
        """
503

    
504
        old_state = self.state
505

    
506
        backend_states = [s.operstate for s in self.backend_networks.all()]
507
        if not backend_states:
508
            self.state = 'PENDING'
509
            self.save()
510
            return
511

    
512
        all_equal = len(set(backend_states)) <= 1
513
        self.state = all_equal and backend_states[0] or 'PENDING'
514

    
515
        # Release the resources on the deletion of the Network
516
        if old_state != 'DELETED' and self.state == 'DELETED':
517
            log.info("Network %r deleted. Releasing link %r mac_prefix %r",
518
                     self.id, self.mac_prefix, self.link)
519
            self.deleted = True
520
            if self.mac_prefix and self.type == 'PRIVATE_MAC_FILTERED':
521
                mac_pool = MacPrefixPoolTable.get_pool()
522
                mac_pool.put(self.mac_prefix)
523
                mac_pool.save()
524

    
525
            if self.link and self.type == 'PRIVATE_PHYSICAL_VLAN':
526
                bridge_pool = BridgePoolTable.get_pool()
527
                bridge_pool.put(self.link)
528
                bridge_pool.save()
529

    
530
        self.save()
531

    
532
    def create_backend_network(self, backend=None):
533
        """Create corresponding BackendNetwork entries."""
534

    
535
        backends = [backend] if backend else Backend.objects.all()
536
        for backend in backends:
537
            BackendNetwork.objects.create(backend=backend, network=self)
538

    
539
    def get_pool(self):
540
        if not self.pool_id:
541
            self.pool = IPPoolTable.objects.create(available_map='',
542
                                                   reserved_map='',
543
                                                   size=0)
544
            self.save()
545
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id).pool
546

    
547
    def reserve_address(self, address):
548
        pool = self.get_pool()
549
        pool.reserve(address)
550
        pool.save()
551

    
552
    def release_address(self, address):
553
        pool = self.get_pool()
554
        pool.put(address)
555
        pool.save()
556

    
557

    
558
class BackendNetwork(models.Model):
559
    OPER_STATES = (
560
        ('PENDING', 'Pending'),
561
        ('ACTIVE', 'Active'),
562
        ('DELETED', 'Deleted'),
563
        ('ERROR', 'Error')
564
    )
565

    
566
    # The list of possible operations on the backend
567
    BACKEND_OPCODES = (
568
        ('OP_NETWORK_ADD', 'Create Network'),
569
        ('OP_NETWORK_CONNECT', 'Activate Network'),
570
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
571
        ('OP_NETWORK_REMOVE', 'Remove Network'),
572
        # These are listed here for completeness,
573
        # and are ignored for the time being
574
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
575
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
576
    )
577

    
578
    # The operating state of a Netowork,
579
    # upon the successful completion of a backend operation.
580
    # IMPORTANT: Make sure all keys have a corresponding
581
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
582
    OPER_STATE_FROM_OPCODE = {
583
        'OP_NETWORK_ADD': 'PENDING',
584
        'OP_NETWORK_CONNECT': 'ACTIVE',
585
        'OP_NETWORK_DISCONNECT': 'PENDING',
586
        'OP_NETWORK_REMOVE': 'DELETED',
587
        'OP_NETWORK_SET_PARAMS': None,
588
        'OP_NETWORK_QUERY_DATA': None
589
    }
590

    
591
    network = models.ForeignKey(Network, related_name='backend_networks')
592
    backend = models.ForeignKey(Backend, related_name='networks')
593
    created = models.DateTimeField(auto_now_add=True)
594
    updated = models.DateTimeField(auto_now=True)
595
    deleted = models.BooleanField('Deleted', default=False)
596
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
597
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
598
                                 default='PENDING')
599
    backendjobid = models.PositiveIntegerField(null=True)
600
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
601
                                     null=True)
602
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
603
                                        max_length=30, null=True)
604
    backendlogmsg = models.TextField(null=True)
605
    backendtime = models.DateTimeField(null=False,
606
                                       default=datetime.datetime.min)
607

    
608
    class Meta:
609
        # Ensure one entry for each network in each backend
610
        unique_together = (("network", "backend"))
611

    
612
    def __init__(self, *args, **kwargs):
613
        """Initialize state for just created BackendNetwork instances."""
614
        super(BackendNetwork, self).__init__(*args, **kwargs)
615
        if not self.mac_prefix:
616
            # Generate the MAC prefix of the BackendNetwork, by combining
617
            # the Network prefix with the index of the Backend
618
            net_prefix = self.network.mac_prefix
619
            backend_suffix = hex(self.backend.index).replace('0x', '')
620
            mac_prefix = net_prefix + backend_suffix
621
            try:
622
                utils.validate_mac(mac_prefix + ":00:00:00")
623
            except utils.InvalidMacAddress:
624
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
625
                                               mac_prefix)
626
            self.mac_prefix = mac_prefix
627

    
628
    def save(self, *args, **kwargs):
629
        super(BackendNetwork, self).save(*args, **kwargs)
630
        self.network.update_state()
631

    
632
    def delete(self, *args, **kwargs):
633
        super(BackendNetwork, self).delete(*args, **kwargs)
634
        self.network.update_state()
635

    
636

    
637
class NetworkInterface(models.Model):
638
    FIREWALL_PROFILES = (
639
        ('ENABLED', 'Enabled'),
640
        ('DISABLED', 'Disabled'),
641
        ('PROTECTED', 'Protected')
642
    )
643

    
644
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
645
    network = models.ForeignKey(Network, related_name='nics')
646
    created = models.DateTimeField(auto_now_add=True)
647
    updated = models.DateTimeField(auto_now=True)
648
    index = models.IntegerField(null=False)
649
    mac = models.CharField(max_length=32, null=False, unique=True)
650
    ipv4 = models.CharField(max_length=15, null=True)
651
    ipv6 = models.CharField(max_length=100, null=True)
652
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
653
                                        max_length=30, null=True)
654
    dirty = models.BooleanField(default=False)
655

    
656
    def __unicode__(self):
657
        return '%s@%s' % (self.machine.name, self.network.name)
658

    
659

    
660
class PoolTable(models.Model):
661
    available_map = models.TextField(default="", null=False)
662
    reserved_map = models.TextField(default="", null=False)
663
    size = models.IntegerField(null=False)
664

    
665
    # Optional Fields
666
    base = models.CharField(null=True, max_length=32)
667
    offset = models.IntegerField(null=True)
668

    
669
    objects = ForUpdateManager()
670

    
671
    class Meta:
672
        abstract = True
673

    
674
    @classmethod
675
    def get_pool(cls):
676
        try:
677
            pool_row = cls.objects.select_for_update().all()[0]
678
            return pool_row.pool
679
        except IndexError:
680
            raise pools.EmptyPool
681

    
682
    @property
683
    def pool(self):
684
        return self.manager(self)
685

    
686

    
687
class BridgePoolTable(PoolTable):
688
    manager = pools.BridgePool
689

    
690

    
691
class MacPrefixPoolTable(PoolTable):
692
    manager = pools.MacPrefixPool
693

    
694

    
695
class IPPoolTable(PoolTable):
696
    manager = pools.IPPool
697

    
698

    
699
@contextmanager
700
def pooled_rapi_client(obj):
701
        if isinstance(obj, VirtualMachine):
702
            backend = obj.backend
703
        else:
704
            backend = obj
705

    
706
        if backend.offline:
707
            raise ServiceUnavailable
708

    
709
        b = backend
710
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
711
                                 b.username, b.password)
712
        try:
713
            yield client
714
        finally:
715
            put_rapi_client(client)
716

    
717

    
718
class VirtualMachineDiagnosticManager(models.Manager):
719
    """
720
    Custom manager for :class:`VirtualMachineDiagnostic` model.
721
    """
722

    
723
    # diagnostic creation helpers
724
    def create_for_vm(self, vm, level, message, **kwargs):
725
        attrs = {'machine': vm, 'level': level, 'message': message}
726
        attrs.update(kwargs)
727
        # update instance updated time
728
        self.create(**attrs)
729
        vm.save()
730

    
731
    def create_error(self, vm, **kwargs):
732
        self.create_for_vm(vm, 'ERROR', **kwargs)
733

    
734
    def create_debug(self, vm, **kwargs):
735
        self.create_for_vm(vm, 'DEBUG', **kwargs)
736

    
737
    def since(self, vm, created_since, **kwargs):
738
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
739
                **kwargs)
740

    
741

    
742
class VirtualMachineDiagnostic(models.Model):
743
    """
744
    Model to store backend information messages that relate to the state of
745
    the virtual machine.
746
    """
747

    
748
    TYPES = (
749
        ('ERROR', 'Error'),
750
        ('WARNING', 'Warning'),
751
        ('INFO', 'Info'),
752
        ('DEBUG', 'Debug'),
753
    )
754

    
755
    objects = VirtualMachineDiagnosticManager()
756

    
757
    created = models.DateTimeField(auto_now_add=True)
758
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
759
    level = models.CharField(max_length=20, choices=TYPES)
760
    source = models.CharField(max_length=100)
761
    source_date = models.DateTimeField(null=True)
762
    message = models.CharField(max_length=255)
763
    details = models.TextField(null=True)
764

    
765
    class Meta:
766
        ordering = ['-created']
767