Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31.6 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
    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(max_length=64, null=True)
361

    
362
    objects = ForUpdateManager()
363

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

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

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

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

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

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

    
397
    def __unicode__(self):
398
        return str(self.id)
399

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

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

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

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

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

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

    
424

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

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

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

    
437

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

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

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

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

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

    
532
    objects = ForUpdateManager()
533

    
534
    def __unicode__(self):
535
        return str(self.id)
536

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
610

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

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

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

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

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

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

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

    
684

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

    
692
    STATES = (
693
        ("ACTIVE", "Active"),
694
        ("BUILDING", "Building"),
695
    )
696

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

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

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

    
725

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

    
739
    objects = ForUpdateManager()
740

    
741
    def __unicode__(self):
742
        return "<%s@%s>" % (self.ipv4, self.network.id)
743

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

    
750

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

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

    
760
    objects = ForUpdateManager()
761

    
762
    class Meta:
763
        abstract = True
764

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

    
773
    @property
774
    def pool(self):
775
        return self.manager(self)
776

    
777

    
778
class BridgePoolTable(PoolTable):
779
    manager = pools.BridgePool
780

    
781

    
782
class MacPrefixPoolTable(PoolTable):
783
    manager = pools.MacPrefixPool
784

    
785

    
786
class IPPoolTable(PoolTable):
787
    manager = pools.IPPool
788

    
789

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

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

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

    
810

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

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

    
824
    def create_error(self, vm, **kwargs):
825
        self.create_for_vm(vm, 'ERROR', **kwargs)
826

    
827
    def create_debug(self, vm, **kwargs):
828
        self.create_for_vm(vm, 'DEBUG', **kwargs)
829

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

    
834

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

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

    
848
    objects = VirtualMachineDiagnosticManager()
849

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

    
858
    class Meta:
859
        ordering = ['-created']