Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import datetime
31

    
32
from django.conf import settings
33
from django.db import models
34
from django.db import IntegrityError
35
from django.db import transaction
36

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

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

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

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

    
53

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

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

    
66
    @property
67
    def name(self):
68
        """Returns flavor name (generated)"""
69
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
70

    
71
    def __unicode__(self):
72
        return str(self.id)
73

    
74

    
75
class Backend(models.Model):
76
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
77
    port = models.PositiveIntegerField('Port', default=5080)
78
    username = models.CharField('Username', max_length=64, blank=True,
79
                                null=True)
80
    password_hash = models.CharField('Password', max_length=128, blank=True,
81
                                null=True)
82
    # Sha1 is up to 40 characters long
83
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
84
    # Unique index of the Backend, used for the mac-prefixes of the
85
    # BackendNetworks
86
    index = models.PositiveIntegerField('Index', null=False, unique=True,
87
                                        default=0)
88
    drained = models.BooleanField('Drained', default=False, null=False)
89
    offline = models.BooleanField('Offline', default=False, null=False)
90
    # Last refresh of backend resources
91
    updated = models.DateTimeField(auto_now_add=True)
92
    # Backend resources
93
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
94
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
95
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
96
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
97
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
98
                                            null=False)
99
    ctotal = models.PositiveIntegerField('Total number of logical processors',
100
                                         default=0, null=False)
101
    # Custom object manager to protect from cascade delete
102
    objects = ProtectedDeleteManager()
103

    
104
    class Meta:
105
        verbose_name = u'Backend'
106
        ordering = ["clustername"]
107

    
108
    def __unicode__(self):
109
        return self.clustername + "(id=" + str(self.id) + ")"
110

    
111
    @property
112
    def backend_id(self):
113
        return self.id
114

    
115
    def get_client(self):
116
        """Get or create a client. """
117
        if self.offline:
118
            raise ServiceUnavailable
119
        return get_rapi_client(self.id, self.hash,
120
                               self.clustername,
121
                               self.port,
122
                               self.username,
123
                               self.password)
124

    
125
    @staticmethod
126
    def put_client(client):
127
            put_rapi_client(client)
128

    
129
    def create_hash(self):
130
        """Create a hash for this backend. """
131
        return sha1('%s%s%s%s' % \
132
                (self.clustername, self.port, self.username, self.password)) \
133
                .hexdigest()
134

    
135
    @property
136
    def password(self):
137
        return decrypt_db_charfield(self.password_hash)
138

    
139
    @password.setter
140
    def password(self, value):
141
        self.password_hash = encrypt_db_charfield(value)
142

    
143
    def save(self, *args, **kwargs):
144
        # Create a new hash each time a Backend is saved
145
        old_hash = self.hash
146
        self.hash = self.create_hash()
147
        super(Backend, self).save(*args, **kwargs)
148
        if self.hash != old_hash:
149
            # Populate the new hash to the new instances
150
            self.virtual_machines.filter(deleted=False).update(backend_hash=self.hash)
151

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

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

    
175

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

    
187

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

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

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

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

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

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

    
263
    name = models.CharField('Virtual Machine Name', max_length=255)
264
    userid = models.CharField('User ID of the owner', max_length=100,
265
                              db_index=True)
266
    backend = models.ForeignKey(Backend, null=True,
267
                                related_name="virtual_machines",)
268
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
269
    created = models.DateTimeField(auto_now_add=True)
270
    updated = models.DateTimeField(auto_now=True)
271
    imageid = models.CharField(max_length=100, null=False)
272
    hostid = models.CharField(max_length=100)
273
    flavor = models.ForeignKey(Flavor)
274
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
275
    suspended = models.BooleanField('Administratively Suspended',
276
                                    default=False)
277

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

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

    
298
    objects = ForUpdateManager()
299

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

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

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

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

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

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

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

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

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

    
340
    class DeletedError(Exception):
341
        pass
342

    
343
    class BuildingError(Exception):
344
        pass
345

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

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

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

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

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

    
379

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

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

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

    
392

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

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

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

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

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

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

    
450
    objects = ForUpdateManager()
451

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

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

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

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

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

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

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

    
478
    class DeletedError(Exception):
479
        pass
480

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

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

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

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

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

504
        """
505

    
506
        old_state = self.state
507

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

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

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

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

    
532
        self.save()
533

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

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

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

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

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

    
559

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

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

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

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

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

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

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

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

    
638

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

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

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

    
661

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

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

    
671
    objects = ForUpdateManager()
672

    
673
    class Meta:
674
        abstract = True
675

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

    
684
    @property
685
    def pool(self):
686
        return self.manager(self)
687

    
688

    
689
class BridgePoolTable(PoolTable):
690
    manager = pools.BridgePool
691

    
692

    
693
class MacPrefixPoolTable(PoolTable):
694
    manager = pools.MacPrefixPool
695

    
696

    
697
class IPPoolTable(PoolTable):
698
    manager = pools.IPPool
699

    
700

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

    
708
        if backend.offline:
709
            raise ServiceUnavailable
710

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

    
719

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

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

    
733
    def create_error(self, vm, **kwargs):
734
        self.create_for_vm(vm, 'ERROR', **kwargs)
735

    
736
    def create_debug(self, vm, **kwargs):
737
        self.create_for_vm(vm, 'DEBUG', **kwargs)
738

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

    
743

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

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

    
757
    objects = VirtualMachineDiagnosticManager()
758

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

    
767
    class Meta:
768
        ordering = ['-created']
769