Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.7 kB)

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

    
30
import datetime
31

    
32
from copy import deepcopy
33
from django.conf import settings
34
from django.db import models
35
from django.db import IntegrityError
36

    
37
import utils
38
from contextlib import contextmanager
39
from hashlib import sha1
40
from snf_django.lib.api import faults
41
from 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

    
235
class VirtualMachine(models.Model):
236
    # The list of possible actions for a VM
237
    ACTIONS = (
238
        ('CREATE', 'Create VM'),
239
        ('START', 'Start VM'),
240
        ('STOP', 'Shutdown VM'),
241
        ('SUSPEND', 'Admin Suspend VM'),
242
        ('REBOOT', 'Reboot VM'),
243
        ('DESTROY', 'Destroy VM'),
244
        ('RESIZE', 'Resize a 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
        ('RESIZE', 'Resizing')
255
    )
256

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

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

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

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

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

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

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

    
350
    objects = ForUpdateManager()
351

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

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

    
364
    @staticmethod
365
    def put_client(client):
366
            put_rapi_client(client)
367

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

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

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

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

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

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

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

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

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

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

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

    
425

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

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

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

    
438

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

    
447
    ACTIONS = (
448
        ('CREATE', 'Create Network'),
449
        ('DESTROY', 'Destroy Network'),
450
    )
451

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

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

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

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

    
527
    objects = ForUpdateManager()
528

    
529
    def __unicode__(self):
530
        return str(self.id)
531

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

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

543
        """
544
        if self.tags:
545
            return self.tags.split(',')
546
        else:
547
            return []
548

    
549
    def create_backend_network(self, backend=None):
550
        """Create corresponding BackendNetwork entries."""
551

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

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

    
570
    def reserve_address(self, address):
571
        pool = self.get_pool()
572
        pool.reserve(address)
573
        pool.save()
574

    
575
    def release_address(self, address):
576
        pool = self.get_pool()
577
        pool.put(address)
578
        pool.save()
579

    
580
    class InvalidBackendIdError(Exception):
581
        def __init__(self, value):
582
            self.value = value
583

    
584
        def __str__(self):
585
            return repr(self.value)
586

    
587
    class InvalidBackendMsgError(Exception):
588
        def __init__(self, opcode, status):
589
            self.opcode = opcode
590
            self.status = status
591

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

    
596
    class InvalidActionError(Exception):
597
        def __init__(self, action):
598
            self._action = action
599

    
600
        def __str__(self):
601
            return repr(str(self._action))
602

    
603

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

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

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

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

    
654
    class Meta:
655
        # Ensure one entry for each network in each backend
656
        unique_together = (("network", "backend"))
657

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

    
674
    def __unicode__(self):
675
        return '<%s@%s>' % (self.network, self.backend)
676

    
677

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

    
685
    STATES = (
686
        ("ACTIVE", "Active"),
687
        ("BUILDING", "Building"),
688
    )
689

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

    
704
    def __unicode__(self):
705
        return '%s@%s' % (self.machine.name, self.network.name)
706

    
707

    
708
class PoolTable(models.Model):
709
    available_map = models.TextField(default="", null=False)
710
    reserved_map = models.TextField(default="", null=False)
711
    size = models.IntegerField(null=False)
712

    
713
    # Optional Fields
714
    base = models.CharField(null=True, max_length=32)
715
    offset = models.IntegerField(null=True)
716

    
717
    objects = ForUpdateManager()
718

    
719
    class Meta:
720
        abstract = True
721

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

    
730
    @property
731
    def pool(self):
732
        return self.manager(self)
733

    
734

    
735
class BridgePoolTable(PoolTable):
736
    manager = pools.BridgePool
737

    
738

    
739
class MacPrefixPoolTable(PoolTable):
740
    manager = pools.MacPrefixPool
741

    
742

    
743
class IPPoolTable(PoolTable):
744
    manager = pools.IPPool
745

    
746

    
747
@contextmanager
748
def pooled_rapi_client(obj):
749
        if isinstance(obj, VirtualMachine):
750
            backend = obj.backend
751
        else:
752
            backend = obj
753

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

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

    
766

    
767
class VirtualMachineDiagnosticManager(models.Manager):
768
    """
769
    Custom manager for :class:`VirtualMachineDiagnostic` model.
770
    """
771

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

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

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

    
786
    def since(self, vm, created_since, **kwargs):
787
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
788
                                           **kwargs)
789

    
790

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

    
797
    TYPES = (
798
        ('ERROR', 'Error'),
799
        ('WARNING', 'Warning'),
800
        ('INFO', 'Info'),
801
        ('DEBUG', 'Debug'),
802
    )
803

    
804
    objects = VirtualMachineDiagnosticManager()
805

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

    
814
    class Meta:
815
        ordering = ['-created']