Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.4 kB)

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

    
30
import datetime
31

    
32
from django.conf import settings
33
from django.db import models
34
from django.db import IntegrityError
35
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
    backend = models.ForeignKey(Backend, null=True,
266
                                related_name="virtual_machines",)
267
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
268
    created = models.DateTimeField(auto_now_add=True)
269
    updated = models.DateTimeField(auto_now=True)
270
    imageid = models.CharField(max_length=100, null=False)
271
    hostid = models.CharField(max_length=100)
272
    flavor = models.ForeignKey(Flavor)
273
    deleted = models.BooleanField('Deleted', default=False)
274
    suspended = models.BooleanField('Administratively Suspended',
275
                                    default=False)
276

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

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

    
297
    def get_client(self):
298
        if self.backend:
299
            return self.backend.get_client()
300
        else:
301
            raise ServiceUnavailable
302

    
303
    def get_last_diagnostic(self, **filters):
304
        try:
305
            return self.diagnostics.filter()[0]
306
        except IndexError:
307
            return None
308

    
309
    @staticmethod
310
    def put_client(client):
311
            put_rapi_client(client)
312

    
313
    # Error classes
314
    class InvalidBackendIdError(Exception):
315
        def __init__(self, value):
316
            self.value = value
317

    
318
        def __str__(self):
319
            return repr(self.value)
320

    
321
    class InvalidBackendMsgError(Exception):
322
        def __init__(self, opcode, status):
323
            self.opcode = opcode
324
            self.status = status
325

    
326
        def __str__(self):
327
            return repr('<opcode: %s, status: %s>' % (self.opcode,
328
                        self.status))
329

    
330
    class InvalidActionError(Exception):
331
        def __init__(self, action):
332
            self._action = action
333

    
334
        def __str__(self):
335
            return repr(str(self._action))
336

    
337
    class DeletedError(Exception):
338
        pass
339

    
340
    class BuildingError(Exception):
341
        pass
342

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

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

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

    
369
    class Meta:
370
        verbose_name = u'Virtual machine instance'
371
        get_latest_by = 'created'
372

    
373
    def __unicode__(self):
374
        return str(self.id)
375

    
376

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

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

    
386
    def __unicode__(self):
387
        return u'%s: %s' % (self.meta_key, self.meta_value)
388

    
389

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

    
398
    ACTIONS = (
399
       ('CREATE', 'Create Network'),
400
       ('DESTROY', 'Destroy Network'),
401
    )
402

    
403
    RSAPI_STATE_FROM_OPER_STATE = {
404
        'PENDING': 'PENDING',
405
        'ACTIVE': 'ACTIVE',
406
        'DELETED': 'DELETED',
407
        'ERROR': 'ERROR'
408
    }
409

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

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

    
440
    pool = models.OneToOneField('IPPoolTable', related_name='network',
441
                default=lambda: IPPoolTable.objects.create(available_map='',
442
                                                           reserved_map='',
443
                                                           size=0),
444
                null=True)
445

    
446
    objects = ForUpdateManager()
447

    
448
    def __unicode__(self):
449
        return str(self.id)
450

    
451
    class InvalidBackendIdError(Exception):
452
        def __init__(self, value):
453
            self.value = value
454

    
455
        def __str__(self):
456
            return repr(self.value)
457

    
458
    class InvalidBackendMsgError(Exception):
459
        def __init__(self, opcode, status):
460
            self.opcode = opcode
461
            self.status = status
462

    
463
        def __str__(self):
464
            return repr('<opcode: %s, status: %s>' % (self.opcode,
465
                    self.status))
466

    
467
    class InvalidActionError(Exception):
468
        def __init__(self, action):
469
            self._action = action
470

    
471
        def __str__(self):
472
            return repr(str(self._action))
473

    
474
    class DeletedError(Exception):
475
        pass
476

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

    
484
    @property
485
    def backend_tag(self):
486
        """Return the network tag to be used in backend
487

488
        """
489
        return getattr(snf_settings, self.type + '_TAGS')
490

    
491
    @transaction.commit_on_success
492
    def update_state(self):
493
        """Update state of the Network.
494

495
        Update the state of the Network depending on the related
496
        backend_networks. When backend networks do not have the same operstate,
497
        the Network's state is PENDING. Otherwise it is the same with
498
        the BackendNetworks operstate.
499

500
        """
501

    
502
        old_state = self.state
503

    
504
        backend_states = [s.operstate for s in self.backend_networks.all()]
505
        if not backend_states:
506
            self.state = 'PENDING'
507
            self.save()
508
            return
509

    
510
        all_equal = len(set(backend_states)) <= 1
511
        self.state = all_equal and backend_states[0] or 'PENDING'
512

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

    
523
            if self.link and self.type == 'PRIVATE_PHYSICAL_VLAN':
524
                bridge_pool = BridgePoolTable.get_pool()
525
                bridge_pool.put(self.link)
526
                bridge_pool.save()
527

    
528
        self.save()
529

    
530
    def create_backend_network(self, backend=None):
531
        """Create corresponding BackendNetwork entries."""
532

    
533
        backends = [backend] if backend else Backend.objects.all()
534
        for backend in backends:
535
            BackendNetwork.objects.create(backend=backend, network=self)
536

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

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

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

    
555

    
556
class BackendNetwork(models.Model):
557
    OPER_STATES = (
558
        ('PENDING', 'Pending'),
559
        ('ACTIVE', 'Active'),
560
        ('DELETED', 'Deleted'),
561
        ('ERROR', 'Error')
562
    )
563

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

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

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

    
606
    class Meta:
607
        # Ensure one entry for each network in each backend
608
        unique_together = (("network", "backend"))
609

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

    
626
    def save(self, *args, **kwargs):
627
        super(BackendNetwork, self).save(*args, **kwargs)
628
        self.network.update_state()
629

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

    
634

    
635
class NetworkInterface(models.Model):
636
    FIREWALL_PROFILES = (
637
        ('ENABLED', 'Enabled'),
638
        ('DISABLED', 'Disabled'),
639
        ('PROTECTED', 'Protected')
640
    )
641

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

    
654
    def __unicode__(self):
655
        return '%s@%s' % (self.machine.name, self.network.name)
656

    
657

    
658
class PoolTable(models.Model):
659
    available_map = models.TextField(default="", null=False)
660
    reserved_map = models.TextField(default="", null=False)
661
    size = models.IntegerField(null=False)
662

    
663
    # Optional Fields
664
    base = models.CharField(null=True, max_length=32)
665
    offset = models.IntegerField(null=True)
666

    
667
    objects = ForUpdateManager()
668

    
669
    class Meta:
670
        abstract = True
671

    
672
    @classmethod
673
    def get_pool(cls):
674
        try:
675
            pool_row = cls.objects.select_for_update().all()[0]
676
            return pool_row.pool
677
        except IndexError:
678
            raise pools.EmptyPool
679

    
680
    @property
681
    def pool(self):
682
        return self.manager(self)
683

    
684

    
685
class BridgePoolTable(PoolTable):
686
    manager = pools.BridgePool
687

    
688

    
689
class MacPrefixPoolTable(PoolTable):
690
    manager = pools.MacPrefixPool
691

    
692

    
693
class IPPoolTable(PoolTable):
694
    manager = pools.IPPool
695

    
696

    
697
@contextmanager
698
def pooled_rapi_client(obj):
699
        if isinstance(obj, VirtualMachine):
700
            backend = obj.backend
701
        else:
702
            backend = obj
703

    
704
        if backend.offline:
705
            raise ServiceUnavailable
706

    
707
        b = backend
708
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
709
                                 b.username, b.password)
710
        try:
711
            yield client
712
        finally:
713
            put_rapi_client(client)
714

    
715

    
716
class VirtualMachineDiagnosticManager(models.Manager):
717
    """
718
    Custom manager for :class:`VirtualMachineDiagnostic` model.
719
    """
720

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

    
729
    def create_error(self, vm, **kwargs):
730
        self.create_for_vm(vm, 'ERROR', **kwargs)
731

    
732
    def create_debug(self, vm, **kwargs):
733
        self.create_for_vm(vm, 'DEBUG', **kwargs)
734

    
735
    def since(self, vm, created_since, **kwargs):
736
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
737
                **kwargs)
738

    
739

    
740
class VirtualMachineDiagnostic(models.Model):
741
    """
742
    Model to store backend information messages that relate to the state of
743
    the virtual machine.
744
    """
745

    
746
    TYPES = (
747
        ('ERROR', 'Error'),
748
        ('WARNING', 'Warning'),
749
        ('INFO', 'Info'),
750
        ('DEBUG', 'Debug'),
751
    )
752

    
753
    objects = VirtualMachineDiagnosticManager()
754

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

    
763
    class Meta:
764
        ordering = ['-created']
765