Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.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

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

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

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

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

    
52

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

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

    
65
    @property
66
    def name(self):
67
        """Returns flavor name (generated)"""
68
        return u'C%dR%dD%d%s' % (self.cpu, self.ram, self.disk,
69
                                 self.disk_template)
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
        sha = sha1('%s%s%s%s' %
132
                   (self.clustername, self.port, self.username, self.password))
133
        return sha.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)\
151
                                 .update(backend_hash=self.hash)
152

    
153
    def delete(self, *args, **kwargs):
154
        # Integrity Error if non-deleted VMs are associated with Backend
155
        if self.virtual_machines.filter(deleted=False).count():
156
            raise IntegrityError("Non-deleted virtual machines are associated "
157
                                 "with backend: %s" % self)
158
        else:
159
            # ON_DELETE = SET NULL
160
            for vm in self.virtual_machines.all():
161
                vm.backend = None
162
                vm.save()
163
            self.virtual_machines.all().backend = None
164
            # Remove BackendNetworks of this Backend.
165
            # Do not use networks.all().delete(), since delete() method of
166
            # BackendNetwork will not be called!
167
            for net in self.networks.all():
168
                net.delete()
169
            super(Backend, self).delete(*args, **kwargs)
170

    
171
    def __init__(self, *args, **kwargs):
172
        super(Backend, self).__init__(*args, **kwargs)
173
        if not self.pk:
174
            # Generate a unique index for the Backend
175
            indexes = Backend.objects.all().values_list('index', flat=True)
176
            try:
177
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
178
                self.index = first_free
179
            except IndexError:
180
                raise Exception("Can not create more than 16 backends")
181

    
182

    
183
# A backend job may be in one of the following possible states
184
BACKEND_STATUSES = (
185
    ('queued', 'request queued'),
186
    ('waiting', 'request waiting for locks'),
187
    ('canceling', 'request being canceled'),
188
    ('running', 'request running'),
189
    ('canceled', 'request canceled'),
190
    ('success', 'request completed successfully'),
191
    ('error', 'request returned error')
192
)
193

    
194

    
195
class QuotaHolderSerial(models.Model):
196
    serial = models.BigIntegerField(null=False, primary_key=True,
197
                                    db_index=True)
198
    pending = models.BooleanField(default=True, db_index=True)
199
    accepted = models.BooleanField(default=False)
200
    rejected = models.BooleanField(default=False)
201

    
202
    class Meta:
203
        verbose_name = u'Quota Serial'
204
        ordering = ["serial"]
205

    
206
    def save(self, *args, **kwargs):
207
        self.pending = not (self.accepted or self.rejected)
208
        super(QuotaHolderSerial, self).save(*args, **kwargs)
209

    
210

    
211
class VirtualMachine(models.Model):
212
    # The list of possible actions for a VM
213
    ACTIONS = (
214
        ('CREATE', 'Create VM'),
215
        ('START', 'Start VM'),
216
        ('STOP', 'Shutdown VM'),
217
        ('SUSPEND', 'Admin Suspend VM'),
218
        ('REBOOT', 'Reboot VM'),
219
        ('DESTROY', 'Destroy VM')
220
    )
221

    
222
    # The internal operating state of a VM
223
    OPER_STATES = (
224
        ('BUILD', 'Queued for creation'),
225
        ('ERROR', 'Creation failed'),
226
        ('STOPPED', 'Stopped'),
227
        ('STARTED', 'Started'),
228
        ('DESTROYED', 'Destroyed')
229
    )
230

    
231
    # The list of possible operations on the backend
232
    BACKEND_OPCODES = (
233
        ('OP_INSTANCE_CREATE', 'Create Instance'),
234
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
235
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
236
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
237
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
238

    
239
        # These are listed here for completeness,
240
        # and are ignored for the time being
241
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
242
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
243
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
244
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
245
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
246
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
247
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
248
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
249
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
250
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
251
    )
252

    
253
    # The operating state of a VM,
254
    # upon the successful completion of a backend operation.
255
    # IMPORTANT: Make sure all keys have a corresponding
256
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
257
    OPER_STATE_FROM_OPCODE = {
258
        'OP_INSTANCE_CREATE': 'STARTED',
259
        'OP_INSTANCE_REMOVE': 'DESTROYED',
260
        'OP_INSTANCE_STARTUP': 'STARTED',
261
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
262
        'OP_INSTANCE_REBOOT': 'STARTED',
263
        'OP_INSTANCE_SET_PARAMS': None,
264
        'OP_INSTANCE_QUERY_DATA': None,
265
        'OP_INSTANCE_REINSTALL': None,
266
        'OP_INSTANCE_ACTIVATE_DISKS': None,
267
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
268
        'OP_INSTANCE_REPLACE_DISKS': None,
269
        'OP_INSTANCE_MIGRATE': None,
270
        'OP_INSTANCE_CONSOLE': None,
271
        'OP_INSTANCE_RECREATE_DISKS': None,
272
        'OP_INSTANCE_FAILOVER': None
273
    }
274

    
275
    # This dictionary contains the correspondence between
276
    # internal operating states and Server States as defined
277
    # by the Rackspace API.
278
    RSAPI_STATE_FROM_OPER_STATE = {
279
        "BUILD": "BUILD",
280
        "ERROR": "ERROR",
281
        "STOPPED": "STOPPED",
282
        "STARTED": "ACTIVE",
283
        "DESTROYED": "DELETED"
284
    }
285

    
286
    name = models.CharField('Virtual Machine Name', max_length=255)
287
    userid = models.CharField('User ID of the owner', max_length=100,
288
                              db_index=True)
289
    backend = models.ForeignKey(Backend, null=True,
290
                                related_name="virtual_machines",)
291
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
292
    created = models.DateTimeField(auto_now_add=True)
293
    updated = models.DateTimeField(auto_now=True)
294
    imageid = models.CharField(max_length=100, null=False)
295
    hostid = models.CharField(max_length=100)
296
    flavor = models.ForeignKey(Flavor)
297
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
298
    suspended = models.BooleanField('Administratively Suspended',
299
                                    default=False)
300
    serial = models.ForeignKey(QuotaHolderSerial,
301
                               related_name='virtual_machine', null=True)
302

    
303
    # VM State
304
    # The following fields are volatile data, in the sense
305
    # that they need not be persistent in the DB, but rather
306
    # get generated at runtime by quering Ganeti and applying
307
    # updates received from Ganeti.
308

    
309
    # In the future they could be moved to a separate caching layer
310
    # and removed from the database.
311
    # [vkoukis] after discussion with [faidon].
312
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
313
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
314
    backendjobid = models.PositiveIntegerField(null=True)
315
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
316
                                     null=True)
317
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
318
                                        max_length=30, null=True)
319
    backendlogmsg = models.TextField(null=True)
320
    buildpercentage = models.IntegerField(default=0)
321
    backendtime = models.DateTimeField(default=datetime.datetime.min)
322

    
323
    objects = ForUpdateManager()
324

    
325
    def get_client(self):
326
        if self.backend:
327
            return self.backend.get_client()
328
        else:
329
            raise ServiceUnavailable
330

    
331
    def get_last_diagnostic(self, **filters):
332
        try:
333
            return self.diagnostics.filter()[0]
334
        except IndexError:
335
            return None
336

    
337
    @staticmethod
338
    def put_client(client):
339
            put_rapi_client(client)
340

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

    
354
    def save(self, *args, **kwargs):
355
        # Store hash for first time saved vm
356
        if (self.id is None or self.backend_hash == '') and self.backend:
357
            self.backend_hash = self.backend.hash
358
        super(VirtualMachine, self).save(*args, **kwargs)
359

    
360
    @property
361
    def backend_vm_id(self):
362
        """Returns the backend id for this VM by prepending backend-prefix."""
363
        if not self.id:
364
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
365
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
366

    
367
    class Meta:
368
        verbose_name = u'Virtual machine instance'
369
        get_latest_by = 'created'
370

    
371
    def __unicode__(self):
372
        return str(self.id)
373

    
374
    # Error classes
375
    class InvalidBackendIdError(Exception):
376
        def __init__(self, value):
377
            self.value = value
378

    
379
        def __str__(self):
380
            return repr(self.value)
381

    
382
    class InvalidBackendMsgError(Exception):
383
        def __init__(self, opcode, status):
384
            self.opcode = opcode
385
            self.status = status
386

    
387
        def __str__(self):
388
            return repr('<opcode: %s, status: %s>' % (self.opcode,
389
                        self.status))
390

    
391
    class InvalidActionError(Exception):
392
        def __init__(self, action):
393
            self._action = action
394

    
395
        def __str__(self):
396
            return repr(str(self._action))
397

    
398
    class DeletedError(Exception):
399
        pass
400

    
401
    class BuildingError(Exception):
402
        pass
403

    
404

    
405
class VirtualMachineMetadata(models.Model):
406
    meta_key = models.CharField(max_length=50)
407
    meta_value = models.CharField(max_length=500)
408
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
409

    
410
    class Meta:
411
        unique_together = (('meta_key', 'vm'),)
412
        verbose_name = u'Key-value pair of metadata for a VM.'
413

    
414
    def __unicode__(self):
415
        return u'%s: %s' % (self.meta_key, self.meta_value)
416

    
417

    
418
class Network(models.Model):
419
    OPER_STATES = (
420
        ('PENDING', 'Pending'),
421
        ('ACTIVE', 'Active'),
422
        ('DELETED', 'Deleted'),
423
        ('ERROR', 'Error')
424
    )
425

    
426
    ACTIONS = (
427
        ('CREATE', 'Create Network'),
428
        ('DESTROY', 'Destroy Network'),
429
    )
430

    
431
    RSAPI_STATE_FROM_OPER_STATE = {
432
        'PENDING': 'PENDING',
433
        'ACTIVE': 'ACTIVE',
434
        'DELETED': 'DELETED',
435
        'ERROR': 'ERROR'
436
    }
437

    
438
    FLAVORS = {
439
        'CUSTOM': {
440
            'mode': 'bridged',
441
            'link': settings.DEFAULT_BRIDGE,
442
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
443
            'tags': None,
444
            'desc': "Basic flavor used for a bridged network",
445
        },
446
        'IP_LESS_ROUTED': {
447
            'mode': 'routed',
448
            'link': settings.DEFAULT_ROUTING_TABLE,
449
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
450
            'tags': 'ip-less-routed',
451
            'desc': "Flavor used for an IP-less routed network using"
452
                    " Proxy ARP",
453
        },
454
        'MAC_FILTERED': {
455
            'mode': 'bridged',
456
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
457
            'mac_prefix': 'pool',
458
            'tags': 'private-filtered',
459
            'desc': "Flavor used for bridged networks that offer isolation"
460
                    " via filtering packets based on their src "
461
                    " MAC (ebtables)",
462
        },
463
        'PHYSICAL_VLAN': {
464
            'mode': 'bridged',
465
            'link': 'pool',
466
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
467
            'tags': 'physical-vlan',
468
            'desc': "Flavor used for bridged network that offer isolation"
469
                    " via dedicated physical vlan",
470
        },
471
    }
472

    
473
    name = models.CharField('Network Name', max_length=128)
474
    userid = models.CharField('User ID of the owner', max_length=128,
475
                              null=True, db_index=True)
476
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
477
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
478
    gateway = models.CharField('Gateway', max_length=32, null=True)
479
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
480
    dhcp = models.BooleanField('DHCP', default=True)
481
    flavor = models.CharField('Flavor', max_length=32, null=False)
482
    mode = models.CharField('Network Mode', max_length=16, null=True)
483
    link = models.CharField('Network Link', max_length=32, null=True)
484
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
485
    tags = models.CharField('Network Tags', max_length=128, null=True)
486
    public = models.BooleanField(default=False, db_index=True)
487
    created = models.DateTimeField(auto_now_add=True)
488
    updated = models.DateTimeField(auto_now=True)
489
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
490
    state = models.CharField(choices=OPER_STATES, max_length=32,
491
                             default='PENDING')
492
    machines = models.ManyToManyField(VirtualMachine,
493
                                      through='NetworkInterface')
494
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
495
                              default=None)
496

    
497
    pool = models.OneToOneField('IPPoolTable', related_name='network',
498
                default=lambda: IPPoolTable.objects.create(available_map='',
499
                                                           reserved_map='',
500
                                                           size=0),
501
                null=True)
502
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
503
                               null=True)
504

    
505
    objects = ForUpdateManager()
506

    
507
    def __unicode__(self):
508
        return str(self.id)
509

    
510
    @property
511
    def backend_id(self):
512
        """Return the backend id by prepending backend-prefix."""
513
        if not self.id:
514
            raise Network.InvalidBackendIdError("self.id is None")
515
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
516

    
517
    @property
518
    def backend_tag(self):
519
        """Return the network tag to be used in backend
520

521
        """
522
        if self.tags:
523
            return self.tags.split(',')
524
        else:
525
            return []
526

    
527
    def create_backend_network(self, backend=None):
528
        """Create corresponding BackendNetwork entries."""
529

    
530
        backends = [backend] if backend\
531
                             else Backend.objects.filter(offline=False)
532
        for backend in backends:
533
            backend_exists =\
534
                BackendNetwork.objects.filter(backend=backend, network=self)\
535
                                      .exists()
536
            if not backend_exists:
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)\
546
                                                      .pool
547

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

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

    
558
    class InvalidBackendIdError(Exception):
559
        def __init__(self, value):
560
            self.value = value
561

    
562
        def __str__(self):
563
            return repr(self.value)
564

    
565
    class InvalidBackendMsgError(Exception):
566
        def __init__(self, opcode, status):
567
            self.opcode = opcode
568
            self.status = status
569

    
570
        def __str__(self):
571
            return repr('<opcode: %s, status: %s>'
572
                        % (self.opcode, self.status))
573

    
574
    class InvalidActionError(Exception):
575
        def __init__(self, action):
576
            self._action = action
577

    
578
        def __str__(self):
579
            return repr(str(self._action))
580

    
581
    class DeletedError(Exception):
582
        pass
583

    
584
    class BuildingError(Exception):
585
        pass
586

    
587

    
588
class BackendNetwork(models.Model):
589
    OPER_STATES = (
590
        ('PENDING', 'Pending'),
591
        ('ACTIVE', 'Active'),
592
        ('DELETED', 'Deleted'),
593
        ('ERROR', 'Error')
594
    )
595

    
596
    # The list of possible operations on the backend
597
    BACKEND_OPCODES = (
598
        ('OP_NETWORK_ADD', 'Create Network'),
599
        ('OP_NETWORK_CONNECT', 'Activate Network'),
600
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
601
        ('OP_NETWORK_REMOVE', 'Remove Network'),
602
        # These are listed here for completeness,
603
        # and are ignored for the time being
604
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
605
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
606
    )
607

    
608
    # The operating state of a Netowork,
609
    # upon the successful completion of a backend operation.
610
    # IMPORTANT: Make sure all keys have a corresponding
611
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
612
    OPER_STATE_FROM_OPCODE = {
613
        'OP_NETWORK_ADD': 'PENDING',
614
        'OP_NETWORK_CONNECT': 'ACTIVE',
615
        'OP_NETWORK_DISCONNECT': 'PENDING',
616
        'OP_NETWORK_REMOVE': 'DELETED',
617
        'OP_NETWORK_SET_PARAMS': None,
618
        'OP_NETWORK_QUERY_DATA': None
619
    }
620

    
621
    network = models.ForeignKey(Network, related_name='backend_networks')
622
    backend = models.ForeignKey(Backend, related_name='networks')
623
    created = models.DateTimeField(auto_now_add=True)
624
    updated = models.DateTimeField(auto_now=True)
625
    deleted = models.BooleanField('Deleted', default=False)
626
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
627
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
628
                                 default='PENDING')
629
    backendjobid = models.PositiveIntegerField(null=True)
630
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
631
                                     null=True)
632
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
633
                                        max_length=30, null=True)
634
    backendlogmsg = models.TextField(null=True)
635
    backendtime = models.DateTimeField(null=False,
636
                                       default=datetime.datetime.min)
637

    
638
    class Meta:
639
        # Ensure one entry for each network in each backend
640
        unique_together = (("network", "backend"))
641

    
642
    def __init__(self, *args, **kwargs):
643
        """Initialize state for just created BackendNetwork instances."""
644
        super(BackendNetwork, self).__init__(*args, **kwargs)
645
        if not self.mac_prefix:
646
            # Generate the MAC prefix of the BackendNetwork, by combining
647
            # the Network prefix with the index of the Backend
648
            net_prefix = self.network.mac_prefix
649
            backend_suffix = hex(self.backend.index).replace('0x', '')
650
            mac_prefix = net_prefix + backend_suffix
651
            try:
652
                utils.validate_mac(mac_prefix + ":00:00:00")
653
            except utils.InvalidMacAddress:
654
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
655
                                              mac_prefix)
656
            self.mac_prefix = mac_prefix
657

    
658

    
659
class NetworkInterface(models.Model):
660
    FIREWALL_PROFILES = (
661
        ('ENABLED', 'Enabled'),
662
        ('DISABLED', 'Disabled'),
663
        ('PROTECTED', 'Protected')
664
    )
665

    
666
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
667
    network = models.ForeignKey(Network, related_name='nics')
668
    created = models.DateTimeField(auto_now_add=True)
669
    updated = models.DateTimeField(auto_now=True)
670
    index = models.IntegerField(null=False)
671
    mac = models.CharField(max_length=32, null=False, unique=True)
672
    ipv4 = models.CharField(max_length=15, null=True)
673
    ipv6 = models.CharField(max_length=100, null=True)
674
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
675
                                        max_length=30, null=True)
676
    dirty = models.BooleanField(default=False)
677

    
678
    def __unicode__(self):
679
        return '%s@%s' % (self.machine.name, self.network.name)
680

    
681

    
682
class PoolTable(models.Model):
683
    available_map = models.TextField(default="", null=False)
684
    reserved_map = models.TextField(default="", null=False)
685
    size = models.IntegerField(null=False)
686

    
687
    # Optional Fields
688
    base = models.CharField(null=True, max_length=32)
689
    offset = models.IntegerField(null=True)
690

    
691
    objects = ForUpdateManager()
692

    
693
    class Meta:
694
        abstract = True
695

    
696
    @classmethod
697
    def get_pool(cls):
698
        try:
699
            pool_row = cls.objects.select_for_update().get()
700
            return pool_row.pool
701
        except cls.DoesNotExist:
702
            raise pools.EmptyPool
703

    
704
    @property
705
    def pool(self):
706
        return self.manager(self)
707

    
708

    
709
class BridgePoolTable(PoolTable):
710
    manager = pools.BridgePool
711

    
712

    
713
class MacPrefixPoolTable(PoolTable):
714
    manager = pools.MacPrefixPool
715

    
716

    
717
class IPPoolTable(PoolTable):
718
    manager = pools.IPPool
719

    
720

    
721
@contextmanager
722
def pooled_rapi_client(obj):
723
        if isinstance(obj, VirtualMachine):
724
            backend = obj.backend
725
        else:
726
            backend = obj
727

    
728
        if backend.offline:
729
            raise ServiceUnavailable
730

    
731
        b = backend
732
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
733
                                 b.username, b.password)
734
        try:
735
            yield client
736
        finally:
737
            put_rapi_client(client)
738

    
739

    
740
class VirtualMachineDiagnosticManager(models.Manager):
741
    """
742
    Custom manager for :class:`VirtualMachineDiagnostic` model.
743
    """
744

    
745
    # diagnostic creation helpers
746
    def create_for_vm(self, vm, level, message, **kwargs):
747
        attrs = {'machine': vm, 'level': level, 'message': message}
748
        attrs.update(kwargs)
749
        # update instance updated time
750
        self.create(**attrs)
751
        vm.save()
752

    
753
    def create_error(self, vm, **kwargs):
754
        self.create_for_vm(vm, 'ERROR', **kwargs)
755

    
756
    def create_debug(self, vm, **kwargs):
757
        self.create_for_vm(vm, 'DEBUG', **kwargs)
758

    
759
    def since(self, vm, created_since, **kwargs):
760
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
761
                                           **kwargs)
762

    
763

    
764
class VirtualMachineDiagnostic(models.Model):
765
    """
766
    Model to store backend information messages that relate to the state of
767
    the virtual machine.
768
    """
769

    
770
    TYPES = (
771
        ('ERROR', 'Error'),
772
        ('WARNING', 'Warning'),
773
        ('INFO', 'Info'),
774
        ('DEBUG', 'Debug'),
775
    )
776

    
777
    objects = VirtualMachineDiagnosticManager()
778

    
779
    created = models.DateTimeField(auto_now_add=True)
780
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
781
    level = models.CharField(max_length=20, choices=TYPES)
782
    source = models.CharField(max_length=100)
783
    source_date = models.DateTimeField(null=True)
784
    message = models.CharField(max_length=255)
785
    details = models.TextField(null=True)
786

    
787
    class Meta:
788
        ordering = ['-created']