Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31.7 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 django.conf 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, fields
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
    disk_templates = fields.SeparatedValuesField("Disk Templates", null=True)
94
    # Last refresh of backend resources
95
    updated = models.DateTimeField(auto_now_add=True)
96
    # Backend resources
97
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
98
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
99
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
100
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
101
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
102
                                            null=False)
103
    ctotal = models.PositiveIntegerField('Total number of logical processors',
104
                                         default=0, null=False)
105
    # Custom object manager to protect from cascade delete
106
    objects = ProtectedDeleteManager()
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
201

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

    
213

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

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

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

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

    
235
    def __unicode__(self):
236
        return u"<serial: %s>" % self.serial
237

    
238

    
239
class VirtualMachine(models.Model):
240
    # The list of possible actions for a VM
241
    ACTIONS = (
242
        ('CREATE', 'Create VM'),
243
        ('START', 'Start VM'),
244
        ('STOP', 'Shutdown VM'),
245
        ('SUSPEND', 'Admin Suspend VM'),
246
        ('REBOOT', 'Reboot VM'),
247
        ('DESTROY', 'Destroy VM'),
248
        ('RESIZE', 'Resize a VM'),
249
        ('ADDFLOATINGIP', 'Add floating IP to VM'),
250
        ('REMOVEFLOATINGIP', 'Add floating IP to VM'),
251
    )
252

    
253
    # The internal operating state of a VM
254
    OPER_STATES = (
255
        ('BUILD', 'Queued for creation'),
256
        ('ERROR', 'Creation failed'),
257
        ('STOPPED', 'Stopped'),
258
        ('STARTED', 'Started'),
259
        ('DESTROYED', 'Destroyed'),
260
        ('RESIZE', 'Resizing')
261
    )
262

    
263
    # The list of possible operations on the backend
264
    BACKEND_OPCODES = (
265
        ('OP_INSTANCE_CREATE', 'Create Instance'),
266
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
267
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
268
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
269
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
270

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

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

    
307
    # This dictionary contains the correspondence between
308
    # internal operating states and Server States as defined
309
    # by the Rackspace API.
310
    RSAPI_STATE_FROM_OPER_STATE = {
311
        "BUILD": "BUILD",
312
        "ERROR": "ERROR",
313
        "STOPPED": "STOPPED",
314
        "STARTED": "ACTIVE",
315
        'RESIZE': 'RESIZE',
316
        'DESTROYED': 'DELETED',
317
    }
318

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

    
336
    # VM State
337
    # The following fields are volatile data, in the sense
338
    # that they need not be persistent in the DB, but rather
339
    # get generated at runtime by quering Ganeti and applying
340
    # updates received from Ganeti.
341

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

    
358
    # Latest action and corresponding Ganeti job ID, for actions issued
359
    # by the API
360
    task = models.CharField(max_length=64, null=True)
361
    task_job_id = models.BigIntegerField(null=True)
362

    
363
    objects = ForUpdateManager()
364

    
365
    def get_client(self):
366
        if self.backend:
367
            return self.backend.get_client()
368
        else:
369
            raise faults.ServiceUnavailable
370

    
371
    def get_last_diagnostic(self, **filters):
372
        try:
373
            return self.diagnostics.filter()[0]
374
        except IndexError:
375
            return None
376

    
377
    @staticmethod
378
    def put_client(client):
379
            put_rapi_client(client)
380

    
381
    def save(self, *args, **kwargs):
382
        # Store hash for first time saved vm
383
        if (self.id is None or self.backend_hash == '') and self.backend:
384
            self.backend_hash = self.backend.hash
385
        super(VirtualMachine, self).save(*args, **kwargs)
386

    
387
    @property
388
    def backend_vm_id(self):
389
        """Returns the backend id for this VM by prepending backend-prefix."""
390
        if not self.id:
391
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
392
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
393

    
394
    class Meta:
395
        verbose_name = u'Virtual machine instance'
396
        get_latest_by = 'created'
397

    
398
    def __unicode__(self):
399
        return "<vm: %s>" % str(self.id)
400

    
401
    # Error classes
402
    class InvalidBackendIdError(Exception):
403
        def __init__(self, value):
404
            self.value = value
405

    
406
        def __str__(self):
407
            return repr(self.value)
408

    
409
    class InvalidBackendMsgError(Exception):
410
        def __init__(self, opcode, status):
411
            self.opcode = opcode
412
            self.status = status
413

    
414
        def __str__(self):
415
            return repr('<opcode: %s, status: %s>' % (self.opcode,
416
                        self.status))
417

    
418
    class InvalidActionError(Exception):
419
        def __init__(self, action):
420
            self._action = action
421

    
422
        def __str__(self):
423
            return repr(str(self._action))
424

    
425

    
426
class VirtualMachineMetadata(models.Model):
427
    meta_key = models.CharField(max_length=50)
428
    meta_value = models.CharField(max_length=500)
429
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
430

    
431
    class Meta:
432
        unique_together = (('meta_key', 'vm'),)
433
        verbose_name = u'Key-value pair of metadata for a VM.'
434

    
435
    def __unicode__(self):
436
        return u'%s: %s' % (self.meta_key, self.meta_value)
437

    
438

    
439
class Network(models.Model):
440
    OPER_STATES = (
441
        ('PENDING', 'Pending'),  # Unused because of lazy networks
442
        ('ACTIVE', 'Active'),
443
        ('DELETED', 'Deleted'),
444
        ('ERROR', 'Error')
445
    )
446

    
447
    ACTIONS = (
448
        ('CREATE', 'Create Network'),
449
        ('DESTROY', 'Destroy Network'),
450
        ('ADD', 'Add server to Network'),
451
        ('REMOVE', 'Remove server from Network'),
452
    )
453

    
454
    RSAPI_STATE_FROM_OPER_STATE = {
455
        'PENDING': 'PENDING',
456
        'ACTIVE': 'ACTIVE',
457
        'DELETED': 'DELETED',
458
        'ERROR': 'ERROR'
459
    }
460

    
461
    FLAVORS = {
462
        'CUSTOM': {
463
            'mode': 'bridged',
464
            'link': settings.DEFAULT_BRIDGE,
465
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
466
            'tags': None,
467
            'desc': "Basic flavor used for a bridged network",
468
        },
469
        'IP_LESS_ROUTED': {
470
            'mode': 'routed',
471
            'link': settings.DEFAULT_ROUTING_TABLE,
472
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
473
            'tags': 'ip-less-routed',
474
            'desc': "Flavor used for an IP-less routed network using"
475
                    " Proxy ARP",
476
        },
477
        'MAC_FILTERED': {
478
            'mode': 'bridged',
479
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
480
            'mac_prefix': 'pool',
481
            'tags': 'private-filtered',
482
            'desc': "Flavor used for bridged networks that offer isolation"
483
                    " via filtering packets based on their src "
484
                    " MAC (ebtables)",
485
        },
486
        'PHYSICAL_VLAN': {
487
            'mode': 'bridged',
488
            'link': 'pool',
489
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
490
            'tags': 'physical-vlan',
491
            'desc': "Flavor used for bridged network that offer isolation"
492
                    " via dedicated physical vlan",
493
        },
494
    }
495

    
496
    name = models.CharField('Network Name', max_length=128)
497
    userid = models.CharField('User ID of the owner', max_length=128,
498
                              null=True, db_index=True)
499
    # subnet will be null for IPv6 only networks
500
    subnet = models.CharField('Subnet', max_length=32, null=True)
501
    # subnet6 will be null for IPv4 only networks
502
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
503
    gateway = models.CharField('Gateway', max_length=32, null=True)
504
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
505
    dhcp = models.BooleanField('DHCP', default=True)
506
    flavor = models.CharField('Flavor', max_length=32, null=False)
507
    mode = models.CharField('Network Mode', max_length=16, null=True)
508
    link = models.CharField('Network Link', max_length=32, null=True)
509
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
510
    tags = models.CharField('Network Tags', max_length=128, null=True)
511
    public = models.BooleanField(default=False, db_index=True)
512
    created = models.DateTimeField(auto_now_add=True)
513
    updated = models.DateTimeField(auto_now=True)
514
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
515
    state = models.CharField(choices=OPER_STATES, max_length=32,
516
                             default='PENDING')
517
    machines = models.ManyToManyField(VirtualMachine,
518
                                      through='NetworkInterface')
519
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
520
                              default=None)
521
    drained = models.BooleanField("Drained", default=False, null=False)
522
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
523
                                           default=False)
524
    pool = models.OneToOneField('IPPoolTable', related_name='network',
525
                                default=lambda: IPPoolTable.objects.create(
526
                                                            available_map='',
527
                                                            reserved_map='',
528
                                                            size=0),
529
                                null=True)
530
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
531
                               null=True)
532

    
533
    objects = ForUpdateManager()
534

    
535
    def __unicode__(self):
536
        return "<Network: %s>" % str(self.id)
537

    
538
    @property
539
    def backend_id(self):
540
        """Return the backend id by prepending backend-prefix."""
541
        if not self.id:
542
            raise Network.InvalidBackendIdError("self.id is None")
543
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
544

    
545
    @property
546
    def backend_tag(self):
547
        """Return the network tag to be used in backend
548

549
        """
550
        if self.tags:
551
            return self.tags.split(',')
552
        else:
553
            return []
554

    
555
    def create_backend_network(self, backend=None):
556
        """Create corresponding BackendNetwork entries."""
557

    
558
        backends = [backend] if backend else\
559
            Backend.objects.filter(offline=False)
560
        for backend in backends:
561
            backend_exists =\
562
                BackendNetwork.objects.filter(backend=backend, network=self)\
563
                                      .exists()
564
            if not backend_exists:
565
                BackendNetwork.objects.create(backend=backend, network=self)
566

    
567
    def get_pool(self, with_lock=True):
568
        if not self.pool_id:
569
            self.pool = IPPoolTable.objects.create(available_map='',
570
                                                   reserved_map='',
571
                                                   size=0)
572
            self.save()
573
        objects = IPPoolTable.objects
574
        if with_lock:
575
            objects = objects.select_for_update()
576
        return objects.get(id=self.pool_id).pool
577

    
578
    def reserve_address(self, address):
579
        pool = self.get_pool()
580
        pool.reserve(address)
581
        pool.save()
582

    
583
    def release_address(self, address):
584
        pool = self.get_pool()
585
        pool.put(address)
586
        pool.save()
587

    
588
    class InvalidBackendIdError(Exception):
589
        def __init__(self, value):
590
            self.value = value
591

    
592
        def __str__(self):
593
            return repr(self.value)
594

    
595
    class InvalidBackendMsgError(Exception):
596
        def __init__(self, opcode, status):
597
            self.opcode = opcode
598
            self.status = status
599

    
600
        def __str__(self):
601
            return repr('<opcode: %s, status: %s>'
602
                        % (self.opcode, self.status))
603

    
604
    class InvalidActionError(Exception):
605
        def __init__(self, action):
606
            self._action = action
607

    
608
        def __str__(self):
609
            return repr(str(self._action))
610

    
611

    
612
class BackendNetwork(models.Model):
613
    OPER_STATES = (
614
        ('PENDING', 'Pending'),
615
        ('ACTIVE', 'Active'),
616
        ('DELETED', 'Deleted'),
617
        ('ERROR', 'Error')
618
    )
619

    
620
    # The list of possible operations on the backend
621
    BACKEND_OPCODES = (
622
        ('OP_NETWORK_ADD', 'Create Network'),
623
        ('OP_NETWORK_CONNECT', 'Activate Network'),
624
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
625
        ('OP_NETWORK_REMOVE', 'Remove Network'),
626
        # These are listed here for completeness,
627
        # and are ignored for the time being
628
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
629
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
630
    )
631

    
632
    # The operating state of a Netowork,
633
    # upon the successful completion of a backend operation.
634
    # IMPORTANT: Make sure all keys have a corresponding
635
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
636
    OPER_STATE_FROM_OPCODE = {
637
        'OP_NETWORK_ADD': 'PENDING',
638
        'OP_NETWORK_CONNECT': 'ACTIVE',
639
        'OP_NETWORK_DISCONNECT': 'PENDING',
640
        'OP_NETWORK_REMOVE': 'DELETED',
641
        'OP_NETWORK_SET_PARAMS': None,
642
        'OP_NETWORK_QUERY_DATA': None
643
    }
644

    
645
    network = models.ForeignKey(Network, related_name='backend_networks')
646
    backend = models.ForeignKey(Backend, related_name='networks')
647
    created = models.DateTimeField(auto_now_add=True)
648
    updated = models.DateTimeField(auto_now=True)
649
    deleted = models.BooleanField('Deleted', default=False)
650
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
651
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
652
                                 default='PENDING')
653
    backendjobid = models.PositiveIntegerField(null=True)
654
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
655
                                     null=True)
656
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
657
                                        max_length=30, null=True)
658
    backendlogmsg = models.TextField(null=True)
659
    backendtime = models.DateTimeField(null=False,
660
                                       default=datetime.datetime.min)
661

    
662
    class Meta:
663
        # Ensure one entry for each network in each backend
664
        unique_together = (("network", "backend"))
665

    
666
    def __init__(self, *args, **kwargs):
667
        """Initialize state for just created BackendNetwork instances."""
668
        super(BackendNetwork, self).__init__(*args, **kwargs)
669
        if not self.mac_prefix:
670
            # Generate the MAC prefix of the BackendNetwork, by combining
671
            # the Network prefix with the index of the Backend
672
            net_prefix = self.network.mac_prefix
673
            backend_suffix = hex(self.backend.index).replace('0x', '')
674
            mac_prefix = net_prefix + backend_suffix
675
            try:
676
                utils.validate_mac(mac_prefix + ":00:00:00")
677
            except utils.InvalidMacAddress:
678
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
679
                                              mac_prefix)
680
            self.mac_prefix = mac_prefix
681

    
682
    def __unicode__(self):
683
        return '<%s@%s>' % (self.network, self.backend)
684

    
685

    
686
class NetworkInterface(models.Model):
687
    FIREWALL_PROFILES = (
688
        ('ENABLED', 'Enabled'),
689
        ('DISABLED', 'Disabled'),
690
        ('PROTECTED', 'Protected')
691
    )
692

    
693
    STATES = (
694
        ("ACTIVE", "Active"),
695
        ("BUILDING", "Building"),
696
        ("ERROR", "Error"),
697
    )
698

    
699
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
700
    network = models.ForeignKey(Network, related_name='nics')
701
    created = models.DateTimeField(auto_now_add=True)
702
    updated = models.DateTimeField(auto_now=True)
703
    index = models.IntegerField(null=True)
704
    mac = models.CharField(max_length=32, null=True, unique=True)
705
    ipv4 = models.CharField(max_length=15, null=True)
706
    ipv6 = models.CharField(max_length=100, null=True)
707
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
708
                                        max_length=30, null=True)
709
    dirty = models.BooleanField(default=False)
710
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
711
                             choices=STATES)
712

    
713
    def __unicode__(self):
714
        return "<%s:vm:%s network:%s ipv4:%s ipv6:%s>" % \
715
            (self.index, self.machine_id, self.network_id, self.ipv4,
716
             self.ipv6)
717

    
718
    @property
719
    def is_floating_ip(self):
720
        network = self.network
721
        if self.ipv4 and network.floating_ip_pool:
722
            return network.floating_ips.filter(machine=self.machine,
723
                                               ipv4=self.ipv4,
724
                                               deleted=False).exists()
725
        return False
726

    
727

    
728
class FloatingIP(models.Model):
729
    userid = models.CharField("UUID of the owner", max_length=128,
730
                              null=False, db_index=True)
731
    ipv4 = models.IPAddressField(null=False, unique=True, db_index=True)
732
    network = models.ForeignKey(Network, related_name="floating_ips",
733
                                null=False)
734
    machine = models.ForeignKey(VirtualMachine, related_name="floating_ips",
735
                                null=True)
736
    created = models.DateTimeField(auto_now_add=True)
737
    deleted = models.BooleanField(default=False, null=False)
738
    serial = models.ForeignKey(QuotaHolderSerial,
739
                               related_name="floating_ips", null=True)
740

    
741
    objects = ForUpdateManager()
742

    
743
    def __unicode__(self):
744
        return "<FIP: %s@%s>" % (self.ipv4, self.network.id)
745

    
746
    def in_use(self):
747
        if self.machine is None:
748
            return False
749
        else:
750
            return (not self.machine.deleted)
751

    
752

    
753
class PoolTable(models.Model):
754
    available_map = models.TextField(default="", null=False)
755
    reserved_map = models.TextField(default="", null=False)
756
    size = models.IntegerField(null=False)
757

    
758
    # Optional Fields
759
    base = models.CharField(null=True, max_length=32)
760
    offset = models.IntegerField(null=True)
761

    
762
    objects = ForUpdateManager()
763

    
764
    class Meta:
765
        abstract = True
766

    
767
    @classmethod
768
    def get_pool(cls):
769
        try:
770
            pool_row = cls.objects.select_for_update().get()
771
            return pool_row.pool
772
        except cls.DoesNotExist:
773
            raise pools.EmptyPool
774

    
775
    @property
776
    def pool(self):
777
        return self.manager(self)
778

    
779

    
780
class BridgePoolTable(PoolTable):
781
    manager = pools.BridgePool
782

    
783

    
784
class MacPrefixPoolTable(PoolTable):
785
    manager = pools.MacPrefixPool
786

    
787

    
788
class IPPoolTable(PoolTable):
789
    manager = pools.IPPool
790

    
791

    
792
@contextmanager
793
def pooled_rapi_client(obj):
794
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
795
            backend = obj.backend
796
        else:
797
            backend = obj
798

    
799
        if backend.offline:
800
            log.warning("Trying to connect with offline backend: %s", backend)
801
            raise faults.ServiceUnavailable("Can not connect to offline"
802
                                            " backend: %s" % backend)
803

    
804
        b = backend
805
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
806
                                 b.username, b.password)
807
        try:
808
            yield client
809
        finally:
810
            put_rapi_client(client)
811

    
812

    
813
class VirtualMachineDiagnosticManager(models.Manager):
814
    """
815
    Custom manager for :class:`VirtualMachineDiagnostic` model.
816
    """
817

    
818
    # diagnostic creation helpers
819
    def create_for_vm(self, vm, level, message, **kwargs):
820
        attrs = {'machine': vm, 'level': level, 'message': message}
821
        attrs.update(kwargs)
822
        # update instance updated time
823
        self.create(**attrs)
824
        vm.save()
825

    
826
    def create_error(self, vm, **kwargs):
827
        self.create_for_vm(vm, 'ERROR', **kwargs)
828

    
829
    def create_debug(self, vm, **kwargs):
830
        self.create_for_vm(vm, 'DEBUG', **kwargs)
831

    
832
    def since(self, vm, created_since, **kwargs):
833
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
834
                                           **kwargs)
835

    
836

    
837
class VirtualMachineDiagnostic(models.Model):
838
    """
839
    Model to store backend information messages that relate to the state of
840
    the virtual machine.
841
    """
842

    
843
    TYPES = (
844
        ('ERROR', 'Error'),
845
        ('WARNING', 'Warning'),
846
        ('INFO', 'Info'),
847
        ('DEBUG', 'Debug'),
848
    )
849

    
850
    objects = VirtualMachineDiagnosticManager()
851

    
852
    created = models.DateTimeField(auto_now_add=True)
853
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
854
    level = models.CharField(max_length=20, choices=TYPES)
855
    source = models.CharField(max_length=100)
856
    source_date = models.DateTimeField(null=True)
857
    message = models.CharField(max_length=255)
858
    details = models.TextField(null=True)
859

    
860
    class Meta:
861
        ordering = ['-created']