Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.3 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 copy import deepcopy
33
from django.conf import settings
34
from django.db import models
35
from django.db import IntegrityError
36

    
37
import utils
38
from contextlib import contextmanager
39
from hashlib import sha1
40
from snf_django.lib.api import faults
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
    deleted = models.BooleanField('Deleted', default=False)
60

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

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

    
71
    def __unicode__(self):
72
        return "<%s:%s>" % (str(self.id), self.name)
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
    # Type of hypervisor
91
    hypervisor = models.CharField('Hypervisor', max_length=32, default="kvm",
92
                                  null=False)
93
    # Last refresh of backend resources
94
    updated = models.DateTimeField(auto_now_add=True)
95
    # Backend resources
96
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
97
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
98
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
99
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
100
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
101
                                            null=False)
102
    ctotal = models.PositiveIntegerField('Total number of logical processors',
103
                                         default=0, null=False)
104
    # Custom object manager to protect from cascade delete
105
    objects = ProtectedDeleteManager()
106

    
107
    HYPERVISORS = (
108
        ("kvm", "Linux KVM hypervisor"),
109
        ("xen-pvm", "Xen PVM hypervisor"),
110
        ("xen-hvm", "Xen KVM hypervisor"),
111
    )
112

    
113
    class Meta:
114
        verbose_name = u'Backend'
115
        ordering = ["clustername"]
116

    
117
    def __unicode__(self):
118
        return self.clustername + "(id=" + str(self.id) + ")"
119

    
120
    @property
121
    def backend_id(self):
122
        return self.id
123

    
124
    def get_client(self):
125
        """Get or create a client. """
126
        if self.offline:
127
            raise faults.ServiceUnavailable
128
        return get_rapi_client(self.id, self.hash,
129
                               self.clustername,
130
                               self.port,
131
                               self.username,
132
                               self.password)
133

    
134
    @staticmethod
135
    def put_client(client):
136
            put_rapi_client(client)
137

    
138
    def create_hash(self):
139
        """Create a hash for this backend. """
140
        sha = sha1('%s%s%s%s' %
141
                   (self.clustername, self.port, self.username, self.password))
142
        return sha.hexdigest()
143

    
144
    @property
145
    def password(self):
146
        return decrypt_db_charfield(self.password_hash)
147

    
148
    @password.setter
149
    def password(self, value):
150
        self.password_hash = encrypt_db_charfield(value)
151

    
152
    def save(self, *args, **kwargs):
153
        # Create a new hash each time a Backend is saved
154
        old_hash = self.hash
155
        self.hash = self.create_hash()
156
        super(Backend, self).save(*args, **kwargs)
157
        if self.hash != old_hash:
158
            # Populate the new hash to the new instances
159
            self.virtual_machines.filter(deleted=False)\
160
                                 .update(backend_hash=self.hash)
161

    
162
    def delete(self, *args, **kwargs):
163
        # Integrity Error if non-deleted VMs are associated with Backend
164
        if self.virtual_machines.filter(deleted=False).count():
165
            raise IntegrityError("Non-deleted virtual machines are associated "
166
                                 "with backend: %s" % self)
167
        else:
168
            # ON_DELETE = SET NULL
169
            for vm in self.virtual_machines.all():
170
                vm.backend = None
171
                vm.save()
172
            self.virtual_machines.all().backend = None
173
            # Remove BackendNetworks of this Backend.
174
            # Do not use networks.all().delete(), since delete() method of
175
            # BackendNetwork will not be called!
176
            for net in self.networks.all():
177
                net.delete()
178
            super(Backend, self).delete(*args, **kwargs)
179

    
180
    def __init__(self, *args, **kwargs):
181
        super(Backend, self).__init__(*args, **kwargs)
182
        if not self.pk:
183
            # Generate a unique index for the Backend
184
            indexes = Backend.objects.all().values_list('index', flat=True)
185
            try:
186
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
187
                self.index = first_free
188
            except IndexError:
189
                raise Exception("Can not create more than 16 backends")
190

    
191
    def use_hotplug(self):
192
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
193

    
194
    def get_create_params(self):
195
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
196
        params["hvparams"] = params.get("hvparams", {})\
197
                                   .get(self.hypervisor, {})
198
        return params
199

    
200

    
201
# A backend job may be in one of the following possible states
202
BACKEND_STATUSES = (
203
    ('queued', 'request queued'),
204
    ('waiting', 'request waiting for locks'),
205
    ('canceling', 'request being canceled'),
206
    ('running', 'request running'),
207
    ('canceled', 'request canceled'),
208
    ('success', 'request completed successfully'),
209
    ('error', 'request returned error')
210
)
211

    
212

    
213
class QuotaHolderSerial(models.Model):
214
    """Model representing a serial for a Quotaholder Commission.
215

216
    serial:   The serial that Quotaholder assigned to this commission
217
    pending:  Whether it has been decided to accept or reject this commission
218
    accept:   If pending is False, this attribute indicates whether to accept
219
              or reject this commission
220
    resolved: Whether this commission has been accepted or rejected to
221
              Quotaholder.
222

223
    """
224
    serial = models.BigIntegerField(null=False, primary_key=True,
225
                                    db_index=True)
226
    pending = models.BooleanField(default=True, db_index=True)
227
    accept = models.BooleanField(default=False)
228
    resolved = models.BooleanField(default=False)
229

    
230
    class Meta:
231
        verbose_name = u'Quota Serial'
232
        ordering = ["serial"]
233

    
234

    
235
class VirtualMachine(models.Model):
236
    # The list of possible actions for a VM
237
    ACTIONS = (
238
        ('CREATE', 'Create VM'),
239
        ('START', 'Start VM'),
240
        ('STOP', 'Shutdown VM'),
241
        ('SUSPEND', 'Admin Suspend VM'),
242
        ('REBOOT', 'Reboot VM'),
243
        ('DESTROY', 'Destroy VM'),
244
        ('RESIZE', 'Resize a VM'),
245
    )
246

    
247
    # The internal operating state of a VM
248
    OPER_STATES = (
249
        ('BUILD', 'Queued for creation'),
250
        ('ERROR', 'Creation failed'),
251
        ('STOPPED', 'Stopped'),
252
        ('STARTED', 'Started'),
253
        ('DESTROYED', 'Destroyed'),
254
        ('RESIZE', 'Resizing')
255
    )
256

    
257
    # The list of possible operations on the backend
258
    BACKEND_OPCODES = (
259
        ('OP_INSTANCE_CREATE', 'Create Instance'),
260
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
261
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
262
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
263
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
264

    
265
        # These are listed here for completeness,
266
        # and are ignored for the time being
267
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
268
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
269
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
270
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
271
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
272
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
273
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
274
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
275
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
276
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
277
    )
278

    
279
    # The operating state of a VM,
280
    # upon the successful completion of a backend operation.
281
    # IMPORTANT: Make sure all keys have a corresponding
282
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
283
    OPER_STATE_FROM_OPCODE = {
284
        'OP_INSTANCE_CREATE': 'STARTED',
285
        'OP_INSTANCE_REMOVE': 'DESTROYED',
286
        'OP_INSTANCE_STARTUP': 'STARTED',
287
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
288
        'OP_INSTANCE_REBOOT': 'STARTED',
289
        'OP_INSTANCE_SET_PARAMS': None,
290
        'OP_INSTANCE_QUERY_DATA': None,
291
        'OP_INSTANCE_REINSTALL': None,
292
        'OP_INSTANCE_ACTIVATE_DISKS': None,
293
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
294
        'OP_INSTANCE_REPLACE_DISKS': None,
295
        'OP_INSTANCE_MIGRATE': None,
296
        'OP_INSTANCE_CONSOLE': None,
297
        'OP_INSTANCE_RECREATE_DISKS': None,
298
        'OP_INSTANCE_FAILOVER': None
299
    }
300

    
301
    # This dictionary contains the correspondence between
302
    # internal operating states and Server States as defined
303
    # by the Rackspace API.
304
    RSAPI_STATE_FROM_OPER_STATE = {
305
        "BUILD": "BUILD",
306
        "ERROR": "ERROR",
307
        "STOPPED": "STOPPED",
308
        "STARTED": "ACTIVE",
309
        "DESTROYED": "DELETED",
310
        "RESIZE": "RESIZE"
311
    }
312

    
313
    name = models.CharField('Virtual Machine Name', max_length=255)
314
    userid = models.CharField('User ID of the owner', max_length=100,
315
                              db_index=True, null=False)
316
    backend = models.ForeignKey(Backend, null=True,
317
                                related_name="virtual_machines",)
318
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
319
    created = models.DateTimeField(auto_now_add=True)
320
    updated = models.DateTimeField(auto_now=True)
321
    imageid = models.CharField(max_length=100, null=False)
322
    hostid = models.CharField(max_length=100)
323
    flavor = models.ForeignKey(Flavor)
324
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
325
    suspended = models.BooleanField('Administratively Suspended',
326
                                    default=False)
327
    serial = models.ForeignKey(QuotaHolderSerial,
328
                               related_name='virtual_machine', null=True)
329

    
330
    # VM State
331
    # The following fields are volatile data, in the sense
332
    # that they need not be persistent in the DB, but rather
333
    # get generated at runtime by quering Ganeti and applying
334
    # updates received from Ganeti.
335

    
336
    # In the future they could be moved to a separate caching layer
337
    # and removed from the database.
338
    # [vkoukis] after discussion with [faidon].
339
    action = models.CharField(choices=ACTIONS, max_length=30, null=True,
340
                              default=None)
341
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
342
                                 null=False, default="BUILD")
343
    backendjobid = models.PositiveIntegerField(null=True)
344
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
345
                                     null=True)
346
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
347
                                        max_length=30, null=True)
348
    backendlogmsg = models.TextField(null=True)
349
    buildpercentage = models.IntegerField(default=0)
350
    backendtime = models.DateTimeField(default=datetime.datetime.min)
351

    
352
    objects = ForUpdateManager()
353

    
354
    def get_client(self):
355
        if self.backend:
356
            return self.backend.get_client()
357
        else:
358
            raise faults.ServiceUnavailable
359

    
360
    def get_last_diagnostic(self, **filters):
361
        try:
362
            return self.diagnostics.filter()[0]
363
        except IndexError:
364
            return None
365

    
366
    @staticmethod
367
    def put_client(client):
368
            put_rapi_client(client)
369

    
370
    def save(self, *args, **kwargs):
371
        # Store hash for first time saved vm
372
        if (self.id is None or self.backend_hash == '') and self.backend:
373
            self.backend_hash = self.backend.hash
374
        super(VirtualMachine, self).save(*args, **kwargs)
375

    
376
    @property
377
    def backend_vm_id(self):
378
        """Returns the backend id for this VM by prepending backend-prefix."""
379
        if not self.id:
380
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
381
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
382

    
383
    class Meta:
384
        verbose_name = u'Virtual machine instance'
385
        get_latest_by = 'created'
386

    
387
    def __unicode__(self):
388
        return str(self.id)
389

    
390
    # Error classes
391
    class InvalidBackendIdError(Exception):
392
        def __init__(self, value):
393
            self.value = value
394

    
395
        def __str__(self):
396
            return repr(self.value)
397

    
398
    class InvalidBackendMsgError(Exception):
399
        def __init__(self, opcode, status):
400
            self.opcode = opcode
401
            self.status = status
402

    
403
        def __str__(self):
404
            return repr('<opcode: %s, status: %s>' % (self.opcode,
405
                        self.status))
406

    
407
    class InvalidActionError(Exception):
408
        def __init__(self, action):
409
            self._action = action
410

    
411
        def __str__(self):
412
            return repr(str(self._action))
413

    
414

    
415
class VirtualMachineMetadata(models.Model):
416
    meta_key = models.CharField(max_length=50)
417
    meta_value = models.CharField(max_length=500)
418
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
419

    
420
    class Meta:
421
        unique_together = (('meta_key', 'vm'),)
422
        verbose_name = u'Key-value pair of metadata for a VM.'
423

    
424
    def __unicode__(self):
425
        return u'%s: %s' % (self.meta_key, self.meta_value)
426

    
427

    
428
class Network(models.Model):
429
    OPER_STATES = (
430
        ('PENDING', 'Pending'),  # Unused because of lazy networks
431
        ('ACTIVE', 'Active'),
432
        ('DELETED', 'Deleted'),
433
        ('ERROR', 'Error')
434
    )
435

    
436
    ACTIONS = (
437
        ('CREATE', 'Create Network'),
438
        ('DESTROY', 'Destroy Network'),
439
    )
440

    
441
    RSAPI_STATE_FROM_OPER_STATE = {
442
        'PENDING': 'PENDING',
443
        'ACTIVE': 'ACTIVE',
444
        'DELETED': 'DELETED',
445
        'ERROR': 'ERROR'
446
    }
447

    
448
    FLAVORS = {
449
        'CUSTOM': {
450
            'mode': 'bridged',
451
            'link': settings.DEFAULT_BRIDGE,
452
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
453
            'tags': None,
454
            'desc': "Basic flavor used for a bridged network",
455
        },
456
        'IP_LESS_ROUTED': {
457
            'mode': 'routed',
458
            'link': settings.DEFAULT_ROUTING_TABLE,
459
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
460
            'tags': 'ip-less-routed',
461
            'desc': "Flavor used for an IP-less routed network using"
462
                    " Proxy ARP",
463
        },
464
        'MAC_FILTERED': {
465
            'mode': 'bridged',
466
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
467
            'mac_prefix': 'pool',
468
            'tags': 'private-filtered',
469
            'desc': "Flavor used for bridged networks that offer isolation"
470
                    " via filtering packets based on their src "
471
                    " MAC (ebtables)",
472
        },
473
        'PHYSICAL_VLAN': {
474
            'mode': 'bridged',
475
            'link': 'pool',
476
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
477
            'tags': 'physical-vlan',
478
            'desc': "Flavor used for bridged network that offer isolation"
479
                    " via dedicated physical vlan",
480
        },
481
    }
482

    
483
    name = models.CharField('Network Name', max_length=128)
484
    userid = models.CharField('User ID of the owner', max_length=128,
485
                              null=True, db_index=True)
486
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
487
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
488
    gateway = models.CharField('Gateway', max_length=32, null=True)
489
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
490
    dhcp = models.BooleanField('DHCP', default=True)
491
    flavor = models.CharField('Flavor', max_length=32, null=False)
492
    mode = models.CharField('Network Mode', max_length=16, null=True)
493
    link = models.CharField('Network Link', max_length=32, null=True)
494
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
495
    tags = models.CharField('Network Tags', max_length=128, null=True)
496
    public = models.BooleanField(default=False, db_index=True)
497
    created = models.DateTimeField(auto_now_add=True)
498
    updated = models.DateTimeField(auto_now=True)
499
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
500
    state = models.CharField(choices=OPER_STATES, max_length=32,
501
                             default='PENDING')
502
    machines = models.ManyToManyField(VirtualMachine,
503
                                      through='NetworkInterface')
504
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
505
                              default=None)
506
    drained = models.BooleanField("Drained", default=False, null=False)
507

    
508
    pool = models.OneToOneField('IPPoolTable', related_name='network',
509
                default=lambda: IPPoolTable.objects.create(available_map='',
510
                                                           reserved_map='',
511
                                                           size=0),
512
                null=True)
513
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
514
                               null=True)
515

    
516
    objects = ForUpdateManager()
517

    
518
    def __unicode__(self):
519
        return str(self.id)
520

    
521
    @property
522
    def backend_id(self):
523
        """Return the backend id by prepending backend-prefix."""
524
        if not self.id:
525
            raise Network.InvalidBackendIdError("self.id is None")
526
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
527

    
528
    @property
529
    def backend_tag(self):
530
        """Return the network tag to be used in backend
531

532
        """
533
        if self.tags:
534
            return self.tags.split(',')
535
        else:
536
            return []
537

    
538
    def create_backend_network(self, backend=None):
539
        """Create corresponding BackendNetwork entries."""
540

    
541
        backends = [backend] if backend\
542
                             else Backend.objects.filter(offline=False)
543
        for backend in backends:
544
            backend_exists =\
545
                BackendNetwork.objects.filter(backend=backend, network=self)\
546
                                      .exists()
547
            if not backend_exists:
548
                BackendNetwork.objects.create(backend=backend, network=self)
549

    
550
    def get_pool(self):
551
        if not self.pool_id:
552
            self.pool = IPPoolTable.objects.create(available_map='',
553
                                                   reserved_map='',
554
                                                   size=0)
555
            self.save()
556
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
557
                                                      .pool
558

    
559
    def reserve_address(self, address):
560
        pool = self.get_pool()
561
        pool.reserve(address)
562
        pool.save()
563

    
564
    def release_address(self, address):
565
        pool = self.get_pool()
566
        pool.put(address)
567
        pool.save()
568

    
569
    class InvalidBackendIdError(Exception):
570
        def __init__(self, value):
571
            self.value = value
572

    
573
        def __str__(self):
574
            return repr(self.value)
575

    
576
    class InvalidBackendMsgError(Exception):
577
        def __init__(self, opcode, status):
578
            self.opcode = opcode
579
            self.status = status
580

    
581
        def __str__(self):
582
            return repr('<opcode: %s, status: %s>'
583
                        % (self.opcode, self.status))
584

    
585
    class InvalidActionError(Exception):
586
        def __init__(self, action):
587
            self._action = action
588

    
589
        def __str__(self):
590
            return repr(str(self._action))
591

    
592

    
593
class BackendNetwork(models.Model):
594
    OPER_STATES = (
595
        ('PENDING', 'Pending'),
596
        ('ACTIVE', 'Active'),
597
        ('DELETED', 'Deleted'),
598
        ('ERROR', 'Error')
599
    )
600

    
601
    # The list of possible operations on the backend
602
    BACKEND_OPCODES = (
603
        ('OP_NETWORK_ADD', 'Create Network'),
604
        ('OP_NETWORK_CONNECT', 'Activate Network'),
605
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
606
        ('OP_NETWORK_REMOVE', 'Remove Network'),
607
        # These are listed here for completeness,
608
        # and are ignored for the time being
609
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
610
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
611
    )
612

    
613
    # The operating state of a Netowork,
614
    # upon the successful completion of a backend operation.
615
    # IMPORTANT: Make sure all keys have a corresponding
616
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
617
    OPER_STATE_FROM_OPCODE = {
618
        'OP_NETWORK_ADD': 'PENDING',
619
        'OP_NETWORK_CONNECT': 'ACTIVE',
620
        'OP_NETWORK_DISCONNECT': 'PENDING',
621
        'OP_NETWORK_REMOVE': 'DELETED',
622
        'OP_NETWORK_SET_PARAMS': None,
623
        'OP_NETWORK_QUERY_DATA': None
624
    }
625

    
626
    network = models.ForeignKey(Network, related_name='backend_networks')
627
    backend = models.ForeignKey(Backend, related_name='networks')
628
    created = models.DateTimeField(auto_now_add=True)
629
    updated = models.DateTimeField(auto_now=True)
630
    deleted = models.BooleanField('Deleted', default=False)
631
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
632
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
633
                                 default='PENDING')
634
    backendjobid = models.PositiveIntegerField(null=True)
635
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
636
                                     null=True)
637
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
638
                                        max_length=30, null=True)
639
    backendlogmsg = models.TextField(null=True)
640
    backendtime = models.DateTimeField(null=False,
641
                                       default=datetime.datetime.min)
642

    
643
    class Meta:
644
        # Ensure one entry for each network in each backend
645
        unique_together = (("network", "backend"))
646

    
647
    def __init__(self, *args, **kwargs):
648
        """Initialize state for just created BackendNetwork instances."""
649
        super(BackendNetwork, self).__init__(*args, **kwargs)
650
        if not self.mac_prefix:
651
            # Generate the MAC prefix of the BackendNetwork, by combining
652
            # the Network prefix with the index of the Backend
653
            net_prefix = self.network.mac_prefix
654
            backend_suffix = hex(self.backend.index).replace('0x', '')
655
            mac_prefix = net_prefix + backend_suffix
656
            try:
657
                utils.validate_mac(mac_prefix + ":00:00:00")
658
            except utils.InvalidMacAddress:
659
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
660
                                              mac_prefix)
661
            self.mac_prefix = mac_prefix
662

    
663
    def __unicode__(self):
664
        return '<%s@%s>' % (self.network, self.backend)
665

    
666

    
667
class NetworkInterface(models.Model):
668
    FIREWALL_PROFILES = (
669
        ('ENABLED', 'Enabled'),
670
        ('DISABLED', 'Disabled'),
671
        ('PROTECTED', 'Protected')
672
    )
673

    
674
    STATES = (
675
        ("ACTIVE", "Active"),
676
        ("BUILDING", "Building"),
677
    )
678

    
679
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
680
    network = models.ForeignKey(Network, related_name='nics')
681
    created = models.DateTimeField(auto_now_add=True)
682
    updated = models.DateTimeField(auto_now=True)
683
    index = models.IntegerField(null=False)
684
    mac = models.CharField(max_length=32, null=True, unique=True)
685
    ipv4 = models.CharField(max_length=15, null=True)
686
    ipv6 = models.CharField(max_length=100, null=True)
687
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
688
                                        max_length=30, null=True)
689
    dirty = models.BooleanField(default=False)
690
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
691
                             choices=STATES)
692

    
693
    def __unicode__(self):
694
        return '%s@%s' % (self.machine.name, self.network.name)
695

    
696

    
697
class PoolTable(models.Model):
698
    available_map = models.TextField(default="", null=False)
699
    reserved_map = models.TextField(default="", null=False)
700
    size = models.IntegerField(null=False)
701

    
702
    # Optional Fields
703
    base = models.CharField(null=True, max_length=32)
704
    offset = models.IntegerField(null=True)
705

    
706
    objects = ForUpdateManager()
707

    
708
    class Meta:
709
        abstract = True
710

    
711
    @classmethod
712
    def get_pool(cls):
713
        try:
714
            pool_row = cls.objects.select_for_update().get()
715
            return pool_row.pool
716
        except cls.DoesNotExist:
717
            raise pools.EmptyPool
718

    
719
    @property
720
    def pool(self):
721
        return self.manager(self)
722

    
723

    
724
class BridgePoolTable(PoolTable):
725
    manager = pools.BridgePool
726

    
727

    
728
class MacPrefixPoolTable(PoolTable):
729
    manager = pools.MacPrefixPool
730

    
731

    
732
class IPPoolTable(PoolTable):
733
    manager = pools.IPPool
734

    
735

    
736
@contextmanager
737
def pooled_rapi_client(obj):
738
        if isinstance(obj, VirtualMachine):
739
            backend = obj.backend
740
        else:
741
            backend = obj
742

    
743
        if backend.offline:
744
            log.warning("Trying to connect with offline backend: %s", backend)
745
            raise faults.ServiceUnavailable
746

    
747
        b = backend
748
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
749
                                 b.username, b.password)
750
        try:
751
            yield client
752
        finally:
753
            put_rapi_client(client)
754

    
755

    
756
class VirtualMachineDiagnosticManager(models.Manager):
757
    """
758
    Custom manager for :class:`VirtualMachineDiagnostic` model.
759
    """
760

    
761
    # diagnostic creation helpers
762
    def create_for_vm(self, vm, level, message, **kwargs):
763
        attrs = {'machine': vm, 'level': level, 'message': message}
764
        attrs.update(kwargs)
765
        # update instance updated time
766
        self.create(**attrs)
767
        vm.save()
768

    
769
    def create_error(self, vm, **kwargs):
770
        self.create_for_vm(vm, 'ERROR', **kwargs)
771

    
772
    def create_debug(self, vm, **kwargs):
773
        self.create_for_vm(vm, 'DEBUG', **kwargs)
774

    
775
    def since(self, vm, created_since, **kwargs):
776
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
777
                                           **kwargs)
778

    
779

    
780
class VirtualMachineDiagnostic(models.Model):
781
    """
782
    Model to store backend information messages that relate to the state of
783
    the virtual machine.
784
    """
785

    
786
    TYPES = (
787
        ('ERROR', 'Error'),
788
        ('WARNING', 'Warning'),
789
        ('INFO', 'Info'),
790
        ('DEBUG', 'Debug'),
791
    )
792

    
793
    objects = VirtualMachineDiagnosticManager()
794

    
795
    created = models.DateTimeField(auto_now_add=True)
796
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
797
    level = models.CharField(max_length=20, choices=TYPES)
798
    source = models.CharField(max_length=100)
799
    source_date = models.DateTimeField(null=True)
800
    message = models.CharField(max_length=255)
801
    details = models.TextField(null=True)
802

    
803
    class Meta:
804
        ordering = ['-created']