Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.9 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 snf_django.lib.api import faults
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 faults.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
    """Model representing a serial for a Quotaholder Commission.
197

198
    serial:   The serial that Quotaholder assigned to this commission
199
    pending:  Whether it has been decided to accept or reject this commission
200
    accept:   If pending is False, this attribute indicates whether to accept
201
              or reject this commission
202
    resolved: Whether this commission has been accepted or rejected to
203
              Quotaholder.
204

205
    """
206
    serial = models.BigIntegerField(null=False, primary_key=True,
207
                                    db_index=True)
208
    pending = models.BooleanField(default=True, db_index=True)
209
    accept = models.BooleanField(default=False)
210
    resolved = models.BooleanField(default=False)
211

    
212
    class Meta:
213
        verbose_name = u'Quota Serial'
214
        ordering = ["serial"]
215

    
216

    
217
class VirtualMachine(models.Model):
218
    # The list of possible actions for a VM
219
    ACTIONS = (
220
        ('CREATE', 'Create VM'),
221
        ('START', 'Start VM'),
222
        ('STOP', 'Shutdown VM'),
223
        ('SUSPEND', 'Admin Suspend VM'),
224
        ('REBOOT', 'Reboot VM'),
225
        ('DESTROY', 'Destroy VM')
226
    )
227

    
228
    # The internal operating state of a VM
229
    OPER_STATES = (
230
        ('BUILD', 'Queued for creation'),
231
        ('ERROR', 'Creation failed'),
232
        ('STOPPED', 'Stopped'),
233
        ('STARTED', 'Started'),
234
        ('DESTROYED', 'Destroyed')
235
    )
236

    
237
    # The list of possible operations on the backend
238
    BACKEND_OPCODES = (
239
        ('OP_INSTANCE_CREATE', 'Create Instance'),
240
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
241
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
242
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
243
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
244

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

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

    
281
    # This dictionary contains the correspondence between
282
    # internal operating states and Server States as defined
283
    # by the Rackspace API.
284
    RSAPI_STATE_FROM_OPER_STATE = {
285
        "BUILD": "BUILD",
286
        "ERROR": "ERROR",
287
        "STOPPED": "STOPPED",
288
        "STARTED": "ACTIVE",
289
        "DESTROYED": "DELETED"
290
    }
291

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

    
309
    # VM State
310
    # The following fields are volatile data, in the sense
311
    # that they need not be persistent in the DB, but rather
312
    # get generated at runtime by quering Ganeti and applying
313
    # updates received from Ganeti.
314

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

    
329
    objects = ForUpdateManager()
330

    
331
    def get_client(self):
332
        if self.backend:
333
            return self.backend.get_client()
334
        else:
335
            raise faults.ServiceUnavailable
336

    
337
    def get_last_diagnostic(self, **filters):
338
        try:
339
            return self.diagnostics.filter()[0]
340
        except IndexError:
341
            return None
342

    
343
    @staticmethod
344
    def put_client(client):
345
            put_rapi_client(client)
346

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

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

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

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

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

    
380
    # Error classes
381
    class InvalidBackendIdError(Exception):
382
        def __init__(self, value):
383
            self.value = value
384

    
385
        def __str__(self):
386
            return repr(self.value)
387

    
388
    class InvalidBackendMsgError(Exception):
389
        def __init__(self, opcode, status):
390
            self.opcode = opcode
391
            self.status = status
392

    
393
        def __str__(self):
394
            return repr('<opcode: %s, status: %s>' % (self.opcode,
395
                        self.status))
396

    
397
    class InvalidActionError(Exception):
398
        def __init__(self, action):
399
            self._action = action
400

    
401
        def __str__(self):
402
            return repr(str(self._action))
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'),  # Unused because of lazy networks
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

    
582
class BackendNetwork(models.Model):
583
    OPER_STATES = (
584
        ('PENDING', 'Pending'),
585
        ('ACTIVE', 'Active'),
586
        ('DELETED', 'Deleted'),
587
        ('ERROR', 'Error')
588
    )
589

    
590
    # The list of possible operations on the backend
591
    BACKEND_OPCODES = (
592
        ('OP_NETWORK_ADD', 'Create Network'),
593
        ('OP_NETWORK_CONNECT', 'Activate Network'),
594
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
595
        ('OP_NETWORK_REMOVE', 'Remove Network'),
596
        # These are listed here for completeness,
597
        # and are ignored for the time being
598
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
599
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
600
    )
601

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

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

    
632
    class Meta:
633
        # Ensure one entry for each network in each backend
634
        unique_together = (("network", "backend"))
635

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

    
652
    def __unicode__(self):
653
        return '<%s@%s>' % (self.network, self.backend)
654

    
655

    
656
class NetworkInterface(models.Model):
657
    FIREWALL_PROFILES = (
658
        ('ENABLED', 'Enabled'),
659
        ('DISABLED', 'Disabled'),
660
        ('PROTECTED', 'Protected')
661
    )
662

    
663
    STATES = (
664
        ("ACTIVE", "Active"),
665
        ("BUILDING", "Building"),
666
    )
667

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

    
682
    def __unicode__(self):
683
        return '%s@%s' % (self.machine.name, self.network.name)
684

    
685

    
686
class PoolTable(models.Model):
687
    available_map = models.TextField(default="", null=False)
688
    reserved_map = models.TextField(default="", null=False)
689
    size = models.IntegerField(null=False)
690

    
691
    # Optional Fields
692
    base = models.CharField(null=True, max_length=32)
693
    offset = models.IntegerField(null=True)
694

    
695
    objects = ForUpdateManager()
696

    
697
    class Meta:
698
        abstract = True
699

    
700
    @classmethod
701
    def get_pool(cls):
702
        try:
703
            pool_row = cls.objects.select_for_update().get()
704
            return pool_row.pool
705
        except cls.DoesNotExist:
706
            raise pools.EmptyPool
707

    
708
    @property
709
    def pool(self):
710
        return self.manager(self)
711

    
712

    
713
class BridgePoolTable(PoolTable):
714
    manager = pools.BridgePool
715

    
716

    
717
class MacPrefixPoolTable(PoolTable):
718
    manager = pools.MacPrefixPool
719

    
720

    
721
class IPPoolTable(PoolTable):
722
    manager = pools.IPPool
723

    
724

    
725
@contextmanager
726
def pooled_rapi_client(obj):
727
        if isinstance(obj, VirtualMachine):
728
            backend = obj.backend
729
        else:
730
            backend = obj
731

    
732
        if backend.offline:
733
            log.warning("Trying to connect with offline backend: %s", backend)
734
            raise faults.ServiceUnavailable
735

    
736
        b = backend
737
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
738
                                 b.username, b.password)
739
        try:
740
            yield client
741
        finally:
742
            put_rapi_client(client)
743

    
744

    
745
class VirtualMachineDiagnosticManager(models.Manager):
746
    """
747
    Custom manager for :class:`VirtualMachineDiagnostic` model.
748
    """
749

    
750
    # diagnostic creation helpers
751
    def create_for_vm(self, vm, level, message, **kwargs):
752
        attrs = {'machine': vm, 'level': level, 'message': message}
753
        attrs.update(kwargs)
754
        # update instance updated time
755
        self.create(**attrs)
756
        vm.save()
757

    
758
    def create_error(self, vm, **kwargs):
759
        self.create_for_vm(vm, 'ERROR', **kwargs)
760

    
761
    def create_debug(self, vm, **kwargs):
762
        self.create_for_vm(vm, 'DEBUG', **kwargs)
763

    
764
    def since(self, vm, created_since, **kwargs):
765
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
766
                                           **kwargs)
767

    
768

    
769
class VirtualMachineDiagnostic(models.Model):
770
    """
771
    Model to store backend information messages that relate to the state of
772
    the virtual machine.
773
    """
774

    
775
    TYPES = (
776
        ('ERROR', 'Error'),
777
        ('WARNING', 'Warning'),
778
        ('INFO', 'Info'),
779
        ('DEBUG', 'Debug'),
780
    )
781

    
782
    objects = VirtualMachineDiagnosticManager()
783

    
784
    created = models.DateTimeField(auto_now_add=True)
785
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
786
    level = models.CharField(max_length=20, choices=TYPES)
787
    source = models.CharField(max_length=100)
788
    source_date = models.DateTimeField(null=True)
789
    message = models.CharField(max_length=255)
790
    details = models.TextField(null=True)
791

    
792
    class Meta:
793
        ordering = ['-created']