Statistics
| Branch: | Tag: | Revision:

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

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

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

    
180

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

    
192

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

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

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

    
207

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

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

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

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

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

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

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

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

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

    
320
    objects = ForUpdateManager()
321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
395
    class DeletedError(Exception):
396
        pass
397

    
398
    class BuildingError(Exception):
399
        pass
400

    
401

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

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

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

    
414

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

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

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

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

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

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

    
502
    objects = ForUpdateManager()
503

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

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

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

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

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

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

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

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

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

    
552
    class InvalidBackendIdError(Exception):
553
        def __init__(self, value):
554
            self.value = value
555

    
556
        def __str__(self):
557
            return repr(self.value)
558

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

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

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

    
572
        def __str__(self):
573
            return repr(str(self._action))
574

    
575
    class DeletedError(Exception):
576
        pass
577

    
578
    class BuildingError(Exception):
579
        pass
580

    
581

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

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

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

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

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

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

    
652

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

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

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

    
675

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

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

    
685
    objects = ForUpdateManager()
686

    
687
    class Meta:
688
        abstract = True
689

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

    
698
    @property
699
    def pool(self):
700
        return self.manager(self)
701

    
702

    
703
class BridgePoolTable(PoolTable):
704
    manager = pools.BridgePool
705

    
706

    
707
class MacPrefixPoolTable(PoolTable):
708
    manager = pools.MacPrefixPool
709

    
710

    
711
class IPPoolTable(PoolTable):
712
    manager = pools.IPPool
713

    
714

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

    
722
        if backend.offline:
723
            raise ServiceUnavailable
724

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

    
733

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

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

    
747
    def create_error(self, vm, **kwargs):
748
        self.create_for_vm(vm, 'ERROR', **kwargs)
749

    
750
    def create_debug(self, vm, **kwargs):
751
        self.create_for_vm(vm, 'DEBUG', **kwargs)
752

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

    
757

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

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

    
771
    objects = VirtualMachineDiagnosticManager()
772

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

    
781
    class Meta:
782
        ordering = ['-created']