Statistics
| Branch: | Tag: | Revision:

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

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

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

    
181

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

    
193

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

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

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

    
208

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

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

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

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

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

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

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

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

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

    
321
    objects = ForUpdateManager()
322

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

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

    
335
    @staticmethod
336
    def put_client(client):
337
            put_rapi_client(client)
338

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

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

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

    
365
    class Meta:
366
        verbose_name = u'Virtual machine instance'
367
        get_latest_by = 'created'
368

    
369
    def __unicode__(self):
370
        return str(self.id)
371

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

    
377
        def __str__(self):
378
            return repr(self.value)
379

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

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

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

    
393
        def __str__(self):
394
            return repr(str(self._action))
395

    
396
    class DeletedError(Exception):
397
        pass
398

    
399
    class BuildingError(Exception):
400
        pass
401

    
402

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

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

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

    
415

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

    
424
    ACTIONS = (
425
       ('CREATE', 'Create Network'),
426
       ('DESTROY', 'Destroy Network'),
427
    )
428

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

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

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

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

    
503
    objects = ForUpdateManager()
504

    
505
    def __unicode__(self):
506
        return str(self.id)
507

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

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

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

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

    
528
        backends = [backend] if backend\
529
                             else Backend.objects.filter(offline=False)
530
        for backend in backends:
531
            if not BackendNetwork.objects.filter(backend=backend, network=self)\
532
                                 .exists():
533
                BackendNetwork.objects.create(backend=backend, network=self)
534

    
535
    def get_pool(self):
536
        if not self.pool_id:
537
            self.pool = IPPoolTable.objects.create(available_map='',
538
                                                   reserved_map='',
539
                                                   size=0)
540
            self.save()
541
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
542
                                                      .pool
543

    
544
    def reserve_address(self, address):
545
        pool = self.get_pool()
546
        pool.reserve(address)
547
        pool.save()
548

    
549
    def release_address(self, address):
550
        pool = self.get_pool()
551
        pool.put(address)
552
        pool.save()
553

    
554
    class InvalidBackendIdError(Exception):
555
        def __init__(self, value):
556
            self.value = value
557

    
558
        def __str__(self):
559
            return repr(self.value)
560

    
561
    class InvalidBackendMsgError(Exception):
562
        def __init__(self, opcode, status):
563
            self.opcode = opcode
564
            self.status = status
565

    
566
        def __str__(self):
567
            return repr('<opcode: %s, status: %s>' % (self.opcode,
568
                    self.status))
569

    
570
    class InvalidActionError(Exception):
571
        def __init__(self, action):
572
            self._action = action
573

    
574
        def __str__(self):
575
            return repr(str(self._action))
576

    
577
    class DeletedError(Exception):
578
        pass
579

    
580
    class BuildingError(Exception):
581
        pass
582

    
583

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

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

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

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

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

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

    
654

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

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

    
674
    def __unicode__(self):
675
        return '%s@%s' % (self.machine.name, self.network.name)
676

    
677

    
678
class PoolTable(models.Model):
679
    available_map = models.TextField(default="", null=False)
680
    reserved_map = models.TextField(default="", null=False)
681
    size = models.IntegerField(null=False)
682

    
683
    # Optional Fields
684
    base = models.CharField(null=True, max_length=32)
685
    offset = models.IntegerField(null=True)
686

    
687
    objects = ForUpdateManager()
688

    
689
    class Meta:
690
        abstract = True
691

    
692
    @classmethod
693
    def get_pool(cls):
694
        try:
695
            pool_row = cls.objects.select_for_update().get()
696
            return pool_row.pool
697
        except IndexError:
698
            raise pools.EmptyPool
699

    
700
    @property
701
    def pool(self):
702
        return self.manager(self)
703

    
704

    
705
class BridgePoolTable(PoolTable):
706
    manager = pools.BridgePool
707

    
708

    
709
class MacPrefixPoolTable(PoolTable):
710
    manager = pools.MacPrefixPool
711

    
712

    
713
class IPPoolTable(PoolTable):
714
    manager = pools.IPPool
715

    
716

    
717
@contextmanager
718
def pooled_rapi_client(obj):
719
        if isinstance(obj, VirtualMachine):
720
            backend = obj.backend
721
        else:
722
            backend = obj
723

    
724
        if backend.offline:
725
            raise ServiceUnavailable
726

    
727
        b = backend
728
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
729
                                 b.username, b.password)
730
        try:
731
            yield client
732
        finally:
733
            put_rapi_client(client)
734

    
735

    
736
class VirtualMachineDiagnosticManager(models.Manager):
737
    """
738
    Custom manager for :class:`VirtualMachineDiagnostic` model.
739
    """
740

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

    
749
    def create_error(self, vm, **kwargs):
750
        self.create_for_vm(vm, 'ERROR', **kwargs)
751

    
752
    def create_debug(self, vm, **kwargs):
753
        self.create_for_vm(vm, 'DEBUG', **kwargs)
754

    
755
    def since(self, vm, created_since, **kwargs):
756
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
757
                **kwargs)
758

    
759

    
760
class VirtualMachineDiagnostic(models.Model):
761
    """
762
    Model to store backend information messages that relate to the state of
763
    the virtual machine.
764
    """
765

    
766
    TYPES = (
767
        ('ERROR', 'Error'),
768
        ('WARNING', 'Warning'),
769
        ('INFO', 'Info'),
770
        ('DEBUG', 'Debug'),
771
    )
772

    
773
    objects = VirtualMachineDiagnosticManager()
774

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

    
783
    class Meta:
784
        ordering = ['-created']