Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.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
                       default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
60
    deleted = models.BooleanField('Deleted', default=False)
61

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

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

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

    
75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
201

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

    
213

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

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

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

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

    
235

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

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

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

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

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

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

    
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
    backend = models.ForeignKey(Backend, null=True,
315
                                related_name="virtual_machines",)
316
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
317
    created = models.DateTimeField(auto_now_add=True)
318
    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,
326
                               related_name='virtual_machine', null=True)
327

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

    
334
    # In the future they could be moved to a separate caching layer
335
    # and removed from the database.
336
    # [vkoukis] after discussion with [faidon].
337
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
338
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
339
    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
    objects = ForUpdateManager()
349

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
423

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

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

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

    
436

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

    
445
    ACTIONS = (
446
        ('CREATE', 'Create Network'),
447
        ('DESTROY', 'Destroy Network'),
448
    )
449

    
450
    RSAPI_STATE_FROM_OPER_STATE = {
451
        'PENDING': 'PENDING',
452
        'ACTIVE': 'ACTIVE',
453
        'DELETED': 'DELETED',
454
        'ERROR': 'ERROR'
455
    }
456

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

    
492
    name = models.CharField('Network Name', max_length=128)
493
    userid = models.CharField('User ID of the owner', max_length=128,
494
                              null=True, db_index=True)
495
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
496
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
497
    gateway = models.CharField('Gateway', max_length=32, null=True)
498
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
499
    dhcp = models.BooleanField('DHCP', default=True)
500
    flavor = models.CharField('Flavor', max_length=32, null=False)
501
    mode = models.CharField('Network Mode', max_length=16, null=True)
502
    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
    created = models.DateTimeField(auto_now_add=True)
507
    updated = models.DateTimeField(auto_now=True)
508
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
509
    state = models.CharField(choices=OPER_STATES, max_length=32,
510
                             default='PENDING')
511
    machines = models.ManyToManyField(VirtualMachine,
512
                                      through='NetworkInterface')
513
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
514
                              default=None)
515

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

    
524
    objects = ForUpdateManager()
525

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

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

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

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

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

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

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

    
567
    def reserve_address(self, address):
568
        pool = self.get_pool()
569
        pool.reserve(address)
570
        pool.save()
571

    
572
    def release_address(self, address):
573
        pool = self.get_pool()
574
        pool.put(address)
575
        pool.save()
576

    
577
    class InvalidBackendIdError(Exception):
578
        def __init__(self, value):
579
            self.value = value
580

    
581
        def __str__(self):
582
            return repr(self.value)
583

    
584
    class InvalidBackendMsgError(Exception):
585
        def __init__(self, opcode, status):
586
            self.opcode = opcode
587
            self.status = status
588

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

    
593
    class InvalidActionError(Exception):
594
        def __init__(self, action):
595
            self._action = action
596

    
597
        def __str__(self):
598
            return repr(str(self._action))
599

    
600

    
601
class BackendNetwork(models.Model):
602
    OPER_STATES = (
603
        ('PENDING', 'Pending'),
604
        ('ACTIVE', 'Active'),
605
        ('DELETED', 'Deleted'),
606
        ('ERROR', 'Error')
607
    )
608

    
609
    # The list of possible operations on the backend
610
    BACKEND_OPCODES = (
611
        ('OP_NETWORK_ADD', 'Create Network'),
612
        ('OP_NETWORK_CONNECT', 'Activate Network'),
613
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
614
        ('OP_NETWORK_REMOVE', 'Remove Network'),
615
        # These are listed here for completeness,
616
        # and are ignored for the time being
617
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
618
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
619
    )
620

    
621
    # The operating state of a Netowork,
622
    # upon the successful completion of a backend operation.
623
    # IMPORTANT: Make sure all keys have a corresponding
624
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
625
    OPER_STATE_FROM_OPCODE = {
626
        'OP_NETWORK_ADD': 'PENDING',
627
        'OP_NETWORK_CONNECT': 'ACTIVE',
628
        'OP_NETWORK_DISCONNECT': 'PENDING',
629
        'OP_NETWORK_REMOVE': 'DELETED',
630
        'OP_NETWORK_SET_PARAMS': None,
631
        'OP_NETWORK_QUERY_DATA': None
632
    }
633

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

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

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

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

    
674

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

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

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

    
701
    def __unicode__(self):
702
        return '%s@%s' % (self.machine.name, self.network.name)
703

    
704

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

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

    
714
    objects = ForUpdateManager()
715

    
716
    class Meta:
717
        abstract = True
718

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

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

    
731

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

    
735

    
736
class MacPrefixPoolTable(PoolTable):
737
    manager = pools.MacPrefixPool
738

    
739

    
740
class IPPoolTable(PoolTable):
741
    manager = pools.IPPool
742

    
743

    
744
@contextmanager
745
def pooled_rapi_client(obj):
746
        if isinstance(obj, VirtualMachine):
747
            backend = obj.backend
748
        else:
749
            backend = obj
750

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

    
755
        b = backend
756
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
757
                                 b.username, b.password)
758
        try:
759
            yield client
760
        finally:
761
            put_rapi_client(client)
762

    
763

    
764
class VirtualMachineDiagnosticManager(models.Manager):
765
    """
766
    Custom manager for :class:`VirtualMachineDiagnostic` model.
767
    """
768

    
769
    # diagnostic creation helpers
770
    def create_for_vm(self, vm, level, message, **kwargs):
771
        attrs = {'machine': vm, 'level': level, 'message': message}
772
        attrs.update(kwargs)
773
        # update instance updated time
774
        self.create(**attrs)
775
        vm.save()
776

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

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

    
783
    def since(self, vm, created_since, **kwargs):
784
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
785
                                           **kwargs)
786

    
787

    
788
class VirtualMachineDiagnostic(models.Model):
789
    """
790
    Model to store backend information messages that relate to the state of
791
    the virtual machine.
792
    """
793

    
794
    TYPES = (
795
        ('ERROR', 'Error'),
796
        ('WARNING', 'Warning'),
797
        ('INFO', 'Info'),
798
        ('DEBUG', 'Debug'),
799
    )
800

    
801
    objects = VirtualMachineDiagnosticManager()
802

    
803
    created = models.DateTimeField(auto_now_add=True)
804
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
805
    level = models.CharField(max_length=20, choices=TYPES)
806
    source = models.CharField(max_length=100)
807
    source_date = models.DateTimeField(null=True)
808
    message = models.CharField(max_length=255)
809
    details = models.TextField(null=True)
810

    
811
    class Meta:
812
        ordering = ['-created']