models.py 29.6 KB
Newer Older
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   1. Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# The views and conclusions contained in the software and documentation are
# those of the authors and should not be interpreted as representing official
# policies, either expressed or implied, of GRNET S.A.

30
31
import datetime

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
48
from synnefo.logic.rapi_pool import (get_rapi_client,
                                     put_rapi_client)
49

50
51
52
import logging
log = logging.getLogger(__name__)

53

54
class Flavor(models.Model):
55
    cpu = models.IntegerField('Number of CPUs', default=0)
56
57
58
    ram = models.IntegerField('RAM size in MiB', default=0)
    disk = models.IntegerField('Disk size in GiB', default=0)
    disk_template = models.CharField('Disk template', max_length=32,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
59
                       default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
60
    deleted = models.BooleanField('Deleted', default=False)
61

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
62
    class Meta:
63
        verbose_name = u'Virtual machine flavor'
64
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
65

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

72
    def __unicode__(self):
73
        return str(self.id)
74

75

76
77
78
79
80
class Backend(models.Model):
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
    port = models.PositiveIntegerField('Port', default=5080)
    username = models.CharField('Username', max_length=64, blank=True,
                                null=True)
81
    password_hash = models.CharField('Password', max_length=128, blank=True,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
82
                                     null=True)
83
84
    # Sha1 is up to 40 characters long
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
85
86
87
88
    # Unique index of the Backend, used for the mac-prefixes of the
    # BackendNetworks
    index = models.PositiveIntegerField('Index', null=False, unique=True,
                                        default=0)
89
90
    drained = models.BooleanField('Drained', default=False, null=False)
    offline = models.BooleanField('Offline', default=False, null=False)
91
92
93
    # Type of hypervisor
    hypervisor = models.CharField('Hypervisor', max_length=32, default="kvm",
                                  null=False)
94
95
96
97
98
99
100
101
102
103
104
105
    # Last refresh of backend resources
    updated = models.DateTimeField(auto_now_add=True)
    # Backend resources
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
                                            null=False)
    ctotal = models.PositiveIntegerField('Total number of logical processors',
                                         default=0, null=False)
    # Custom object manager to protect from cascade delete
106
    objects = ProtectedDeleteManager()
107

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

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

    def __unicode__(self):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
119
        return self.clustername + "(id=" + str(self.id) + ")"
120
121
122
123
124

    @property
    def backend_id(self):
        return self.id

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
131
                               self.clustername,
                               self.port,
132
133
134
135
136
137
                               self.username,
                               self.password)

    @staticmethod
    def put_client(client):
            put_rapi_client(client)
138
139
140

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

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

    @password.setter
    def password(self, value):
        self.password_hash = encrypt_db_charfield(value)

153
154
155
156
157
158
159
    def save(self, *args, **kwargs):
        # Create a new hash each time a Backend is saved
        old_hash = self.hash
        self.hash = self.create_hash()
        super(Backend, self).save(*args, **kwargs)
        if self.hash != old_hash:
            # Populate the new hash to the new instances
Christos Stavrakakis's avatar
Christos Stavrakakis committed
160
161
            self.virtual_machines.filter(deleted=False)\
                                 .update(backend_hash=self.hash)
162
163
164
165
166
167
168
169

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

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

192
193
194
195
196
197
198
199
200
    def use_hotplug(self):
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG

    def get_create_params(self):
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
        params["hvparams"] = params.get("hvparams", {})\
                                   .get(self.hypervisor, {})
        return params

201

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


214
class QuotaHolderSerial(models.Model):
215
216
217
218
219
220
221
222
223
224
    """Model representing a serial for a Quotaholder Commission.

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

    """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
225
226
    serial = models.BigIntegerField(null=False, primary_key=True,
                                    db_index=True)
227
    pending = models.BooleanField(default=True, db_index=True)
228
229
    accept = models.BooleanField(default=False)
    resolved = models.BooleanField(default=False)
230
231
232
233
234
235

    class Meta:
        verbose_name = u'Quota Serial'
        ordering = ["serial"]


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

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

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

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

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

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

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

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

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

348
349
    objects = ForUpdateManager()

350
351
    def get_client(self):
        if self.backend:
352
            return self.backend.get_client()
353
        else:
354
            raise faults.ServiceUnavailable
355

356
357
358
359
360
361
    def get_last_diagnostic(self, **filters):
        try:
            return self.diagnostics.filter()[0]
        except IndexError:
            return None

362
363
364
365
    @staticmethod
    def put_client(client):
            put_rapi_client(client)

366
    def __init__(self, *args, **kw):
367
368
        """Initialize state for just created VM instances."""
        super(VirtualMachine, self).__init__(*args, **kw)
369
370
        # This gets called BEFORE an instance gets save()d for
        # the first time.
371
        if not self.pk:
372
373
374
375
376
377
            self.action = None
            self.backendjobid = None
            self.backendjobstatus = None
            self.backendopcode = None
            self.backendlogmsg = None
            self.operstate = 'BUILD'
378

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

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

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

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
    # Error classes
    class InvalidBackendIdError(Exception):
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    class InvalidBackendMsgError(Exception):
        def __init__(self, opcode, status):
            self.opcode = opcode
            self.status = status

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

    class InvalidActionError(Exception):
        def __init__(self, action):
            self._action = action

        def __str__(self):
            return repr(str(self._action))

423

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
424
425
426
class VirtualMachineMetadata(models.Model):
    meta_key = models.CharField(max_length=50)
    meta_value = models.CharField(max_length=500)
427
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
428

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
429
    class Meta:
430
        unique_together = (('meta_key', 'vm'),)
431
        verbose_name = u'Key-value pair of metadata for a VM.'
432

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
433
    def __unicode__(self):
434
        return u'%s: %s' % (self.meta_key, self.meta_value)
Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
435
436


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

    ACTIONS = (
Christos Stavrakakis's avatar
Christos Stavrakakis committed
446
447
        ('CREATE', 'Create Network'),
        ('DESTROY', 'Destroy Network'),
448
449
450
451
452
453
454
455
456
    )

    RSAPI_STATE_FROM_OPER_STATE = {
        'PENDING': 'PENDING',
        'ACTIVE': 'ACTIVE',
        'DELETED': 'DELETED',
        'ERROR': 'ERROR'
    }

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

492
    name = models.CharField('Network Name', max_length=128)
493
494
    userid = models.CharField('User ID of the owner', max_length=128,
                              null=True, db_index=True)
495
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
Christos Stavrakakis's avatar
Christos Stavrakakis committed
496
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
497
    gateway = models.CharField('Gateway', max_length=32, null=True)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
498
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
499
    dhcp = models.BooleanField('DHCP', default=True)
500
501
502
    flavor = models.CharField('Flavor', max_length=32, null=False)
    mode = models.CharField('Network Mode', max_length=16, null=True)
    link = models.CharField('Network Link', max_length=32, null=True)
503
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
504
    tags = models.CharField('Network Tags', max_length=128, null=True)
505
    public = models.BooleanField(default=False, db_index=True)
506
507
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
508
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
509
510
    state = models.CharField(choices=OPER_STATES, max_length=32,
                             default='PENDING')
511
    machines = models.ManyToManyField(VirtualMachine,
512
                                      through='NetworkInterface')
513
514
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
                              default=None)
515

Christos Stavrakakis's avatar
Christos Stavrakakis committed
516
    pool = models.OneToOneField('IPPoolTable', related_name='network',
517
518
519
520
                default=lambda: IPPoolTable.objects.create(available_map='',
                                                           reserved_map='',
                                                           size=0),
                null=True)
521
522
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
                               null=True)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
523

524
    objects = ForUpdateManager()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
525

526
527
528
    def __unicode__(self):
        return str(self.id)

529
530
    @property
    def backend_id(self):
531
        """Return the backend id by prepending backend-prefix."""
532
533
        if not self.id:
            raise Network.InvalidBackendIdError("self.id is None")
534
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
535
536
537
538
539
540

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

        """
541
542
543
544
        if self.tags:
            return self.tags.split(',')
        else:
            return []
545

546
547
548
    def create_backend_network(self, backend=None):
        """Create corresponding BackendNetwork entries."""

549
550
        backends = [backend] if backend\
                             else Backend.objects.filter(offline=False)
551
        for backend in backends:
Christos Stavrakakis's avatar
Christos Stavrakakis committed
552
553
554
555
            backend_exists =\
                BackendNetwork.objects.filter(backend=backend, network=self)\
                                      .exists()
            if not backend_exists:
556
                BackendNetwork.objects.create(backend=backend, network=self)
557

Christos Stavrakakis's avatar
Christos Stavrakakis committed
558
    def get_pool(self):
559
560
561
562
563
        if not self.pool_id:
            self.pool = IPPoolTable.objects.create(available_map='',
                                                   reserved_map='',
                                                   size=0)
            self.save()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
564
565
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
                                                      .pool
Christos Stavrakakis's avatar
Christos Stavrakakis committed
566

Christos Stavrakakis's avatar
Christos Stavrakakis committed
567
568
    def reserve_address(self, address):
        pool = self.get_pool()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
569
        pool.reserve(address)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
570
        pool.save()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
571

Christos Stavrakakis's avatar
Christos Stavrakakis committed
572
573
574
575
    def release_address(self, address):
        pool = self.get_pool()
        pool.put(address)
        pool.save()
Christos Stavrakakis's avatar
Christos Stavrakakis committed
576

Christos Stavrakakis's avatar
Christos Stavrakakis committed
577
578
579
580
581
582
583
584
585
586
587
588
589
    class InvalidBackendIdError(Exception):
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    class InvalidBackendMsgError(Exception):
        def __init__(self, opcode, status):
            self.opcode = opcode
            self.status = status

        def __str__(self):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
590
591
            return repr('<opcode: %s, status: %s>'
                        % (self.opcode, self.status))
Christos Stavrakakis's avatar
Christos Stavrakakis committed
592
593
594
595
596
597
598
599

    class InvalidActionError(Exception):
        def __init__(self, action):
            self._action = action

        def __str__(self):
            return repr(str(self._action))

600

601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
class BackendNetwork(models.Model):
    OPER_STATES = (
        ('PENDING', 'Pending'),
        ('ACTIVE', 'Active'),
        ('DELETED', 'Deleted'),
        ('ERROR', 'Error')
    )

    # The list of possible operations on the backend
    BACKEND_OPCODES = (
        ('OP_NETWORK_ADD', 'Create Network'),
        ('OP_NETWORK_CONNECT', 'Activate Network'),
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
        ('OP_NETWORK_REMOVE', 'Remove Network'),
        # These are listed here for completeness,
        # and are ignored for the time being
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
    )

    # The operating state of a Netowork,
    # upon the successful completion of a backend operation.
    # IMPORTANT: Make sure all keys have a corresponding
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
    OPER_STATE_FROM_OPCODE = {
        'OP_NETWORK_ADD': 'PENDING',
        'OP_NETWORK_CONNECT': 'ACTIVE',
        'OP_NETWORK_DISCONNECT': 'PENDING',
        'OP_NETWORK_REMOVE': 'DELETED',
        'OP_NETWORK_SET_PARAMS': None,
        'OP_NETWORK_QUERY_DATA': None
    }

    network = models.ForeignKey(Network, related_name='backend_networks')
    backend = models.ForeignKey(Backend, related_name='networks')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    deleted = models.BooleanField('Deleted', default=False)
639
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
640
641
642
643
644
645
646
647
648
649
650
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
                                 default='PENDING')
    backendjobid = models.PositiveIntegerField(null=True)
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
                                     null=True)
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
                                        max_length=30, null=True)
    backendlogmsg = models.TextField(null=True)
    backendtime = models.DateTimeField(null=False,
                                       default=datetime.datetime.min)

651
652
653
654
    class Meta:
        # Ensure one entry for each network in each backend
        unique_together = (("network", "backend"))

655
656
657
658
659
660
661
662
663
664
665
666
    def __init__(self, *args, **kwargs):
        """Initialize state for just created BackendNetwork instances."""
        super(BackendNetwork, self).__init__(*args, **kwargs)
        if not self.mac_prefix:
            # Generate the MAC prefix of the BackendNetwork, by combining
            # the Network prefix with the index of the Backend
            net_prefix = self.network.mac_prefix
            backend_suffix = hex(self.backend.index).replace('0x', '')
            mac_prefix = net_prefix + backend_suffix
            try:
                utils.validate_mac(mac_prefix + ":00:00:00")
            except utils.InvalidMacAddress:
Christos Stavrakakis's avatar
Christos Stavrakakis committed
667
668
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
                                              mac_prefix)
669
670
            self.mac_prefix = mac_prefix

671
672
673
    def __unicode__(self):
        return '<%s@%s>' % (self.network, self.backend)

674
675
676
677

class NetworkInterface(models.Model):
    FIREWALL_PROFILES = (
        ('ENABLED', 'Enabled'),
678
679
        ('DISABLED', 'Disabled'),
        ('PROTECTED', 'Protected')
680
    )
681

682
683
684
685
686
    STATES = (
        ("ACTIVE", "Active"),
        ("BUILDING", "Building"),
    )

687
688
689
690
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
    network = models.ForeignKey(Network, related_name='nics')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
691
    index = models.IntegerField(null=False)
692
    mac = models.CharField(max_length=32, null=True, unique=True)
693
694
695
    ipv4 = models.CharField(max_length=15, null=True)
    ipv6 = models.CharField(max_length=100, null=True)
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
696
                                        max_length=30, null=True)
697
    dirty = models.BooleanField(default=False)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
698
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
699
                             choices=STATES)
700

Giorgos Verigakis's avatar
Giorgos Verigakis committed
701
702
    def __unicode__(self):
        return '%s@%s' % (self.machine.name, self.network.name)
703
704


705
706
707
708
class PoolTable(models.Model):
    available_map = models.TextField(default="", null=False)
    reserved_map = models.TextField(default="", null=False)
    size = models.IntegerField(null=False)
709

710
711
712
    # Optional Fields
    base = models.CharField(null=True, max_length=32)
    offset = models.IntegerField(null=True)
713

714
715
    objects = ForUpdateManager()

716
717
718
719
    class Meta:
        abstract = True

    @classmethod
720
    def get_pool(cls):
721
        try:
722
            pool_row = cls.objects.select_for_update().get()
723
            return pool_row.pool
Christos Stavrakakis's avatar
Christos Stavrakakis committed
724
        except cls.DoesNotExist:
725
            raise pools.EmptyPool
726

727
728
729
    @property
    def pool(self):
        return self.manager(self)
730
731


732
733
class BridgePoolTable(PoolTable):
    manager = pools.BridgePool
734

Christos Stavrakakis's avatar
Christos Stavrakakis committed
735

736
737
class MacPrefixPoolTable(PoolTable):
    manager = pools.MacPrefixPool
Christos Stavrakakis's avatar
Christos Stavrakakis committed
738
739
740
741


class IPPoolTable(PoolTable):
    manager = pools.IPPool
742
743
744
745
746
747
748
749
750
751


@contextmanager
def pooled_rapi_client(obj):
        if isinstance(obj, VirtualMachine):
            backend = obj.backend
        else:
            backend = obj

        if backend.offline:
752
            log.warning("Trying to connect with offline backend: %s", backend)
753
            raise faults.ServiceUnavailable
754
755
756
757

        b = backend
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
                                 b.username, b.password)
758
759
760
761
        try:
            yield client
        finally:
            put_rapi_client(client)
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784


class VirtualMachineDiagnosticManager(models.Manager):
    """
    Custom manager for :class:`VirtualMachineDiagnostic` model.
    """

    # diagnostic creation helpers
    def create_for_vm(self, vm, level, message, **kwargs):
        attrs = {'machine': vm, 'level': level, 'message': message}
        attrs.update(kwargs)
        # update instance updated time
        self.create(**attrs)
        vm.save()

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

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

    def since(self, vm, created_since, **kwargs):
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
785
                                           **kwargs)
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812


class VirtualMachineDiagnostic(models.Model):
    """
    Model to store backend information messages that relate to the state of
    the virtual machine.
    """

    TYPES = (
        ('ERROR', 'Error'),
        ('WARNING', 'Warning'),
        ('INFO', 'Info'),
        ('DEBUG', 'Debug'),
    )

    objects = VirtualMachineDiagnosticManager()

    created = models.DateTimeField(auto_now_add=True)
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
    level = models.CharField(max_length=20, choices=TYPES)
    source = models.CharField(max_length=100)
    source_date = models.DateTimeField(null=True)
    message = models.CharField(max_length=255)
    details = models.TextField(null=True)

    class Meta:
        ordering = ['-created']