Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.8 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' % (self.cpu, self.ram, self.disk)
69

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

    
73

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

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

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

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

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

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

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

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

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

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

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

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

    
174

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

    
186

    
187
class QuotaHolderSerial(models.Model):
188
    serial = models.BigIntegerField(null=False, primary_key=True, db_index=True)
189
    pending = models.BooleanField(default=True, db_index=True)
190
    accepted = models.BooleanField(default=False)
191
    rejected = models.BooleanField(default=False)
192

    
193
    class Meta:
194
        verbose_name = u'Quota Serial'
195
        ordering = ["serial"]
196

    
197
    def save(self, *args, **kwargs):
198
        self.pending = not (self.accepted or self.rejected)
199
        super(QuotaHolderSerial, self).save(*args, **kwargs)
200

    
201

    
202
class VirtualMachine(models.Model):
203
    # The list of possible actions for a VM
204
    ACTIONS = (
205
       ('CREATE', 'Create VM'),
206
       ('START', 'Start VM'),
207
       ('STOP', 'Shutdown VM'),
208
       ('SUSPEND', 'Admin Suspend VM'),
209
       ('REBOOT', 'Reboot VM'),
210
       ('DESTROY', 'Destroy VM')
211
    )
212

    
213
    # The internal operating state of a VM
214
    OPER_STATES = (
215
        ('BUILD', 'Queued for creation'),
216
        ('ERROR', 'Creation failed'),
217
        ('STOPPED', 'Stopped'),
218
        ('STARTED', 'Started'),
219
        ('DESTROYED', 'Destroyed')
220
    )
221

    
222
    # The list of possible operations on the backend
223
    BACKEND_OPCODES = (
224
        ('OP_INSTANCE_CREATE', 'Create Instance'),
225
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
226
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
227
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
228
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
229

    
230
        # These are listed here for completeness,
231
        # and are ignored for the time being
232
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
233
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
234
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
235
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
236
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
237
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
238
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
239
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
240
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
241
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
242
    )
243

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

    
266
    # This dictionary contains the correspondence between
267
    # internal operating states and Server States as defined
268
    # by the Rackspace API.
269
    RSAPI_STATE_FROM_OPER_STATE = {
270
        "BUILD": "BUILD",
271
        "ERROR": "ERROR",
272
        "STOPPED": "STOPPED",
273
        "STARTED": "ACTIVE",
274
        "DESTROYED": "DELETED"
275
    }
276

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

    
294
    # VM State
295
    # The following fields are volatile data, in the sense
296
    # that they need not be persistent in the DB, but rather
297
    # get generated at runtime by quering Ganeti and applying
298
    # updates received from Ganeti.
299

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

    
314
    objects = ForUpdateManager()
315

    
316
    def get_client(self):
317
        if self.backend:
318
            return self.backend.get_client()
319
        else:
320
            raise ServiceUnavailable
321

    
322
    def get_last_diagnostic(self, **filters):
323
        try:
324
            return self.diagnostics.filter()[0]
325
        except IndexError:
326
            return None
327

    
328
    @staticmethod
329
    def put_client(client):
330
            put_rapi_client(client)
331

    
332
    def __init__(self, *args, **kw):
333
        """Initialize state for just created VM instances."""
334
        super(VirtualMachine, self).__init__(*args, **kw)
335
        # This gets called BEFORE an instance gets save()d for
336
        # the first time.
337
        if not self.pk:
338
            self.action = None
339
            self.backendjobid = None
340
            self.backendjobstatus = None
341
            self.backendopcode = None
342
            self.backendlogmsg = None
343
            self.operstate = 'BUILD'
344

    
345
    def save(self, *args, **kwargs):
346
        # Store hash for first time saved vm
347
        if (self.id is None or self.backend_hash == '') and self.backend:
348
            self.backend_hash = self.backend.hash
349
        super(VirtualMachine, self).save(*args, **kwargs)
350

    
351
    @property
352
    def backend_vm_id(self):
353
        """Returns the backend id for this VM by prepending backend-prefix."""
354
        if not self.id:
355
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
356
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
357

    
358
    class Meta:
359
        verbose_name = u'Virtual machine instance'
360
        get_latest_by = 'created'
361

    
362
    def __unicode__(self):
363
        return str(self.id)
364

    
365
    # Error classes
366
    class InvalidBackendIdError(Exception):
367
        def __init__(self, value):
368
            self.value = value
369

    
370
        def __str__(self):
371
            return repr(self.value)
372

    
373
    class InvalidBackendMsgError(Exception):
374
        def __init__(self, opcode, status):
375
            self.opcode = opcode
376
            self.status = status
377

    
378
        def __str__(self):
379
            return repr('<opcode: %s, status: %s>' % (self.opcode,
380
                        self.status))
381

    
382
    class InvalidActionError(Exception):
383
        def __init__(self, action):
384
            self._action = action
385

    
386
        def __str__(self):
387
            return repr(str(self._action))
388

    
389
    class DeletedError(Exception):
390
        pass
391

    
392
    class BuildingError(Exception):
393
        pass
394

    
395

    
396
class VirtualMachineMetadata(models.Model):
397
    meta_key = models.CharField(max_length=50)
398
    meta_value = models.CharField(max_length=500)
399
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
400

    
401
    class Meta:
402
        unique_together = (('meta_key', 'vm'),)
403
        verbose_name = u'Key-value pair of metadata for a VM.'
404

    
405
    def __unicode__(self):
406
        return u'%s: %s' % (self.meta_key, self.meta_value)
407

    
408

    
409
class Network(models.Model):
410
    OPER_STATES = (
411
        ('PENDING', 'Pending'),
412
        ('ACTIVE', 'Active'),
413
        ('DELETED', 'Deleted'),
414
        ('ERROR', 'Error')
415
    )
416

    
417
    ACTIONS = (
418
       ('CREATE', 'Create Network'),
419
       ('DESTROY', 'Destroy Network'),
420
    )
421

    
422
    RSAPI_STATE_FROM_OPER_STATE = {
423
        'PENDING': 'PENDING',
424
        'ACTIVE': 'ACTIVE',
425
        'DELETED': 'DELETED',
426
        'ERROR': 'ERROR'
427
    }
428

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

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

    
488
    pool = models.OneToOneField('IPPoolTable', related_name='network',
489
                default=lambda: IPPoolTable.objects.create(available_map='',
490
                                                           reserved_map='',
491
                                                           size=0),
492
                null=True)
493
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
494
                               null=True)
495

    
496
    objects = ForUpdateManager()
497

    
498
    def __unicode__(self):
499
        return str(self.id)
500

    
501
    @property
502
    def backend_id(self):
503
        """Return the backend id by prepending backend-prefix."""
504
        if not self.id:
505
            raise Network.InvalidBackendIdError("self.id is None")
506
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
507

    
508
    @property
509
    def backend_tag(self):
510
        """Return the network tag to be used in backend
511

512
        """
513
        if self.tags:
514
            return self.tags.split(',')
515
        else:
516
            return []
517

    
518
    def create_backend_network(self, backend=None):
519
        """Create corresponding BackendNetwork entries."""
520

    
521
        backends = [backend] if backend else Backend.objects.all()
522
        for backend in backends:
523
            BackendNetwork.objects.create(backend=backend, network=self)
524

    
525
    def get_pool(self):
526
        if not self.pool_id:
527
            self.pool = IPPoolTable.objects.create(available_map='',
528
                                                   reserved_map='',
529
                                                   size=0)
530
            self.save()
531
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
532
                                                      .pool
533

    
534
    def reserve_address(self, address):
535
        pool = self.get_pool()
536
        pool.reserve(address)
537
        pool.save()
538

    
539
    def release_address(self, address):
540
        pool = self.get_pool()
541
        pool.put(address)
542
        pool.save()
543

    
544
    class InvalidBackendIdError(Exception):
545
        def __init__(self, value):
546
            self.value = value
547

    
548
        def __str__(self):
549
            return repr(self.value)
550

    
551
    class InvalidBackendMsgError(Exception):
552
        def __init__(self, opcode, status):
553
            self.opcode = opcode
554
            self.status = status
555

    
556
        def __str__(self):
557
            return repr('<opcode: %s, status: %s>' % (self.opcode,
558
                    self.status))
559

    
560
    class InvalidActionError(Exception):
561
        def __init__(self, action):
562
            self._action = action
563

    
564
        def __str__(self):
565
            return repr(str(self._action))
566

    
567
    class DeletedError(Exception):
568
        pass
569

    
570
    class BuildingError(Exception):
571
        pass
572

    
573

    
574
class BackendNetwork(models.Model):
575
    OPER_STATES = (
576
        ('PENDING', 'Pending'),
577
        ('ACTIVE', 'Active'),
578
        ('DELETED', 'Deleted'),
579
        ('ERROR', 'Error')
580
    )
581

    
582
    # The list of possible operations on the backend
583
    BACKEND_OPCODES = (
584
        ('OP_NETWORK_ADD', 'Create Network'),
585
        ('OP_NETWORK_CONNECT', 'Activate Network'),
586
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
587
        ('OP_NETWORK_REMOVE', 'Remove Network'),
588
        # These are listed here for completeness,
589
        # and are ignored for the time being
590
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
591
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
592
    )
593

    
594
    # The operating state of a Netowork,
595
    # upon the successful completion of a backend operation.
596
    # IMPORTANT: Make sure all keys have a corresponding
597
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
598
    OPER_STATE_FROM_OPCODE = {
599
        'OP_NETWORK_ADD': 'PENDING',
600
        'OP_NETWORK_CONNECT': 'ACTIVE',
601
        'OP_NETWORK_DISCONNECT': 'PENDING',
602
        'OP_NETWORK_REMOVE': 'DELETED',
603
        'OP_NETWORK_SET_PARAMS': None,
604
        'OP_NETWORK_QUERY_DATA': None
605
    }
606

    
607
    network = models.ForeignKey(Network, related_name='backend_networks')
608
    backend = models.ForeignKey(Backend, related_name='networks')
609
    created = models.DateTimeField(auto_now_add=True)
610
    updated = models.DateTimeField(auto_now=True)
611
    deleted = models.BooleanField('Deleted', default=False)
612
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
613
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
614
                                 default='PENDING')
615
    backendjobid = models.PositiveIntegerField(null=True)
616
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
617
                                     null=True)
618
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
619
                                        max_length=30, null=True)
620
    backendlogmsg = models.TextField(null=True)
621
    backendtime = models.DateTimeField(null=False,
622
                                       default=datetime.datetime.min)
623

    
624
    class Meta:
625
        # Ensure one entry for each network in each backend
626
        unique_together = (("network", "backend"))
627

    
628
    def __init__(self, *args, **kwargs):
629
        """Initialize state for just created BackendNetwork instances."""
630
        super(BackendNetwork, self).__init__(*args, **kwargs)
631
        if not self.mac_prefix:
632
            # Generate the MAC prefix of the BackendNetwork, by combining
633
            # the Network prefix with the index of the Backend
634
            net_prefix = self.network.mac_prefix
635
            backend_suffix = hex(self.backend.index).replace('0x', '')
636
            mac_prefix = net_prefix + backend_suffix
637
            try:
638
                utils.validate_mac(mac_prefix + ":00:00:00")
639
            except utils.InvalidMacAddress:
640
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
641
                                               mac_prefix)
642
            self.mac_prefix = mac_prefix
643

    
644

    
645
class NetworkInterface(models.Model):
646
    FIREWALL_PROFILES = (
647
        ('ENABLED', 'Enabled'),
648
        ('DISABLED', 'Disabled'),
649
        ('PROTECTED', 'Protected')
650
    )
651

    
652
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
653
    network = models.ForeignKey(Network, related_name='nics')
654
    created = models.DateTimeField(auto_now_add=True)
655
    updated = models.DateTimeField(auto_now=True)
656
    index = models.IntegerField(null=False)
657
    mac = models.CharField(max_length=32, null=False, unique=True)
658
    ipv4 = models.CharField(max_length=15, null=True)
659
    ipv6 = models.CharField(max_length=100, null=True)
660
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
661
                                        max_length=30, null=True)
662
    dirty = models.BooleanField(default=False)
663

    
664
    def __unicode__(self):
665
        return '%s@%s' % (self.machine.name, self.network.name)
666

    
667

    
668
class PoolTable(models.Model):
669
    available_map = models.TextField(default="", null=False)
670
    reserved_map = models.TextField(default="", null=False)
671
    size = models.IntegerField(null=False)
672

    
673
    # Optional Fields
674
    base = models.CharField(null=True, max_length=32)
675
    offset = models.IntegerField(null=True)
676

    
677
    objects = ForUpdateManager()
678

    
679
    class Meta:
680
        abstract = True
681

    
682
    @classmethod
683
    def get_pool(cls):
684
        try:
685
            pool_row = cls.objects.select_for_update().get()
686
            return pool_row.pool
687
        except IndexError:
688
            raise pools.EmptyPool
689

    
690
    @property
691
    def pool(self):
692
        return self.manager(self)
693

    
694

    
695
class BridgePoolTable(PoolTable):
696
    manager = pools.BridgePool
697

    
698

    
699
class MacPrefixPoolTable(PoolTable):
700
    manager = pools.MacPrefixPool
701

    
702

    
703
class IPPoolTable(PoolTable):
704
    manager = pools.IPPool
705

    
706

    
707
@contextmanager
708
def pooled_rapi_client(obj):
709
        if isinstance(obj, VirtualMachine):
710
            backend = obj.backend
711
        else:
712
            backend = obj
713

    
714
        if backend.offline:
715
            raise ServiceUnavailable
716

    
717
        b = backend
718
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
719
                                 b.username, b.password)
720
        try:
721
            yield client
722
        finally:
723
            put_rapi_client(client)
724

    
725

    
726
class VirtualMachineDiagnosticManager(models.Manager):
727
    """
728
    Custom manager for :class:`VirtualMachineDiagnostic` model.
729
    """
730

    
731
    # diagnostic creation helpers
732
    def create_for_vm(self, vm, level, message, **kwargs):
733
        attrs = {'machine': vm, 'level': level, 'message': message}
734
        attrs.update(kwargs)
735
        # update instance updated time
736
        self.create(**attrs)
737
        vm.save()
738

    
739
    def create_error(self, vm, **kwargs):
740
        self.create_for_vm(vm, 'ERROR', **kwargs)
741

    
742
    def create_debug(self, vm, **kwargs):
743
        self.create_for_vm(vm, 'DEBUG', **kwargs)
744

    
745
    def since(self, vm, created_since, **kwargs):
746
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
747
                **kwargs)
748

    
749

    
750
class VirtualMachineDiagnostic(models.Model):
751
    """
752
    Model to store backend information messages that relate to the state of
753
    the virtual machine.
754
    """
755

    
756
    TYPES = (
757
        ('ERROR', 'Error'),
758
        ('WARNING', 'Warning'),
759
        ('INFO', 'Info'),
760
        ('DEBUG', 'Debug'),
761
    )
762

    
763
    objects = VirtualMachineDiagnosticManager()
764

    
765
    created = models.DateTimeField(auto_now_add=True)
766
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
767
    level = models.CharField(max_length=20, choices=TYPES)
768
    source = models.CharField(max_length=100)
769
    source_date = models.DateTimeField(null=True)
770
    message = models.CharField(max_length=255)
771
    details = models.TextField(null=True)
772

    
773
    class Meta:
774
        ordering = ['-created']