Statistics
| Branch: | Tag: | Revision:

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

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
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
    def __unicode__(self):
235
        return u"<serial: %s>" % self.serial
236

    
237

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

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

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

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

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

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

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

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

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

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

    
362
    #is this virtualmachine a router
363
    router = models.BooleanField('router', default=False)
364

    
365
    objects = ForUpdateManager()
366

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

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

    
379
    @staticmethod
380
    def put_client(client):
381
            put_rapi_client(client)
382

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

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

    
396
    class Meta:
397
        verbose_name = u'Virtual machine instance'
398
        get_latest_by = 'created'
399

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

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

    
408
        def __str__(self):
409
            return repr(self.value)
410

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

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

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

    
424
        def __str__(self):
425
            return repr(str(self._action))
426

    
427

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

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

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

    
440

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

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

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

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

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

    
535
    objects = ForUpdateManager()
536

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

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

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

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

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

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

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

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

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

    
590
    class InvalidBackendIdError(Exception):
591
        def __init__(self, value):
592
            self.value = value
593

    
594
        def __str__(self):
595
            return repr(self.value)
596

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

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

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

    
610
        def __str__(self):
611
            return repr(str(self._action))
612

    
613

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

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

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

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

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

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

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

    
687

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

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

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

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

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

    
729

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

    
743
    objects = ForUpdateManager()
744

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

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

    
754

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

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

    
764
    objects = ForUpdateManager()
765

    
766
    class Meta:
767
        abstract = True
768

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

    
777
    @property
778
    def pool(self):
779
        return self.manager(self)
780

    
781

    
782
class BridgePoolTable(PoolTable):
783
    manager = pools.BridgePool
784

    
785

    
786
class MacPrefixPoolTable(PoolTable):
787
    manager = pools.MacPrefixPool
788

    
789

    
790
class IPPoolTable(PoolTable):
791
    manager = pools.IPPool
792

    
793

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

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

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

    
814

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

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

    
828
    def create_error(self, vm, **kwargs):
829
        self.create_for_vm(vm, 'ERROR', **kwargs)
830

    
831
    def create_debug(self, vm, **kwargs):
832
        self.create_for_vm(vm, 'DEBUG', **kwargs)
833

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

    
838

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

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

    
852
    objects = VirtualMachineDiagnosticManager()
853

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

    
862
    class Meta:
863
        ordering = ['-created']