Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.6 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
                              db_index=True)
266
    backend = models.ForeignKey(Backend, null=True,
267
                                related_name="virtual_machines",)
268
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
269
    created = models.DateTimeField(auto_now_add=True)
270
    updated = models.DateTimeField(auto_now=True)
271
    imageid = models.CharField(max_length=100, null=False)
272
    hostid = models.CharField(max_length=100)
273
    flavor = models.ForeignKey(Flavor)
274
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
275
    suspended = models.BooleanField('Administratively Suspended',
276
                                    default=False)
277

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

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

    
298
    objects = ForUpdateManager()
299

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

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

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

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

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

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

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

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

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

    
340
    class DeletedError(Exception):
341
        pass
342

    
343
    class BuildingError(Exception):
344
        pass
345

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

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

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

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

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

    
379

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

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

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

    
392

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

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

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

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

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

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

    
450
    objects = ForUpdateManager()
451

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

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

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

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

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

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

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

    
478
    class DeletedError(Exception):
479
        pass
480

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

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

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

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

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

504
        """
505

    
506
        old_state = self.state
507

    
508
        backend_states = [s.operstate for s in
509
                          self.backend_networks.filter(backend__offline=False)]
510
        if not backend_states:
511
            self.state = 'PENDING'
512
            self.save()
513
            return
514

    
515
        all_equal = len(set(backend_states)) <= 1
516
        self.state = all_equal and backend_states[0] or 'PENDING'
517

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

    
528
            if self.link and self.type == 'PRIVATE_PHYSICAL_VLAN':
529
                bridge_pool = BridgePoolTable.get_pool()
530
                bridge_pool.put(self.link)
531
                bridge_pool.save()
532

    
533
        self.save()
534

    
535
    def create_backend_network(self, backend=None):
536
        """Create corresponding BackendNetwork entries."""
537

    
538
        backends = [backend] if backend\
539
                             else Backend.objects.filter(offline=False)
540
        for backend in backends:
541
            BackendNetwork.objects.create(backend=backend, network=self)
542

    
543
    def get_pool(self):
544
        if not self.pool_id:
545
            self.pool = IPPoolTable.objects.create(available_map='',
546
                                                   reserved_map='',
547
                                                   size=0)
548
            self.save()
549
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id).pool
550

    
551
    def reserve_address(self, address):
552
        pool = self.get_pool()
553
        pool.reserve(address)
554
        pool.save()
555

    
556
    def release_address(self, address):
557
        pool = self.get_pool()
558
        pool.put(address)
559
        pool.save()
560

    
561

    
562
class BackendNetwork(models.Model):
563
    OPER_STATES = (
564
        ('PENDING', 'Pending'),
565
        ('ACTIVE', 'Active'),
566
        ('DELETED', 'Deleted'),
567
        ('ERROR', 'Error')
568
    )
569

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

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

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

    
612
    class Meta:
613
        # Ensure one entry for each network in each backend
614
        unique_together = (("network", "backend"))
615

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

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

    
636
    def delete(self, *args, **kwargs):
637
        super(BackendNetwork, self).delete(*args, **kwargs)
638
        self.network.update_state()
639

    
640

    
641
class NetworkInterface(models.Model):
642
    FIREWALL_PROFILES = (
643
        ('ENABLED', 'Enabled'),
644
        ('DISABLED', 'Disabled'),
645
        ('PROTECTED', 'Protected')
646
    )
647

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

    
660
    def __unicode__(self):
661
        return '%s@%s' % (self.machine.name, self.network.name)
662

    
663

    
664
class PoolTable(models.Model):
665
    available_map = models.TextField(default="", null=False)
666
    reserved_map = models.TextField(default="", null=False)
667
    size = models.IntegerField(null=False)
668

    
669
    # Optional Fields
670
    base = models.CharField(null=True, max_length=32)
671
    offset = models.IntegerField(null=True)
672

    
673
    objects = ForUpdateManager()
674

    
675
    class Meta:
676
        abstract = True
677

    
678
    @classmethod
679
    def get_pool(cls):
680
        try:
681
            pool_row = cls.objects.select_for_update().all()[0]
682
            return pool_row.pool
683
        except IndexError:
684
            raise pools.EmptyPool
685

    
686
    @property
687
    def pool(self):
688
        return self.manager(self)
689

    
690

    
691
class BridgePoolTable(PoolTable):
692
    manager = pools.BridgePool
693

    
694

    
695
class MacPrefixPoolTable(PoolTable):
696
    manager = pools.MacPrefixPool
697

    
698

    
699
class IPPoolTable(PoolTable):
700
    manager = pools.IPPool
701

    
702

    
703
@contextmanager
704
def pooled_rapi_client(obj):
705
        if isinstance(obj, VirtualMachine):
706
            backend = obj.backend
707
        else:
708
            backend = obj
709

    
710
        if backend.offline:
711
            raise ServiceUnavailable
712

    
713
        b = backend
714
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
715
                                 b.username, b.password)
716
        try:
717
            yield client
718
        finally:
719
            put_rapi_client(client)
720

    
721

    
722
class VirtualMachineDiagnosticManager(models.Manager):
723
    """
724
    Custom manager for :class:`VirtualMachineDiagnostic` model.
725
    """
726

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

    
735
    def create_error(self, vm, **kwargs):
736
        self.create_for_vm(vm, 'ERROR', **kwargs)
737

    
738
    def create_debug(self, vm, **kwargs):
739
        self.create_for_vm(vm, 'DEBUG', **kwargs)
740

    
741
    def since(self, vm, created_since, **kwargs):
742
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
743
                **kwargs)
744

    
745

    
746
class VirtualMachineDiagnostic(models.Model):
747
    """
748
    Model to store backend information messages that relate to the state of
749
    the virtual machine.
750
    """
751

    
752
    TYPES = (
753
        ('ERROR', 'Error'),
754
        ('WARNING', 'Warning'),
755
        ('INFO', 'Info'),
756
        ('DEBUG', 'Debug'),
757
    )
758

    
759
    objects = VirtualMachineDiagnosticManager()
760

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

    
769
    class Meta:
770
        ordering = ['-created']
771