Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.5 kB)

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

    
30
import datetime
31

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

    
37
import utils
38
from contextlib import contextmanager
39
from hashlib import sha1
40
from snf_django.lib.api import faults
41
from synnefo import settings as snf_settings
42
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
43

    
44
from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
45
from synnefo.db import pools
46

    
47
from synnefo.logic.rapi_pool import (get_rapi_client,
48
                                     put_rapi_client)
49

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

    
53

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

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

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

    
71
    def __unicode__(self):
72
        return "<%s:%s>" % (str(self.id), self.name)
73

    
74

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
200

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

    
212

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

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

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

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

    
234
    def __unicode__(self):
235
        return u"<serial: %s>" % self.serial
236

    
237

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

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

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

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

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

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

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

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

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

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

    
360
    objects = ForUpdateManager()
361

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

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

    
374
    @staticmethod
375
    def put_client(client):
376
            put_rapi_client(client)
377

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

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

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

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

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

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

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

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

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

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

    
422

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

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

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

    
435

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

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

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

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

    
491
    name = models.CharField('Network Name', max_length=128)
492
    userid = models.CharField('User ID of the owner', max_length=128,
493
                              null=True, db_index=True)
494
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
495
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
496
    gateway = models.CharField('Gateway', max_length=32, null=True)
497
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
498
    dhcp = models.BooleanField('DHCP', default=True)
499
    flavor = models.CharField('Flavor', max_length=32, null=False)
500
    mode = models.CharField('Network Mode', max_length=16, null=True)
501
    link = models.CharField('Network Link', max_length=32, null=True)
502
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
503
    tags = models.CharField('Network Tags', max_length=128, null=True)
504
    public = models.BooleanField(default=False, db_index=True)
505
    created = models.DateTimeField(auto_now_add=True)
506
    updated = models.DateTimeField(auto_now=True)
507
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
508
    state = models.CharField(choices=OPER_STATES, max_length=32,
509
                             default='PENDING')
510
    machines = models.ManyToManyField(VirtualMachine,
511
                                      through='NetworkInterface')
512
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
513
                              default=None)
514
    drained = models.BooleanField("Drained", default=False, null=False)
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 FloatingIP(models.Model):
706
    userid = models.CharField("UUID of the owner", max_length=128,
707
                              null=False, db_index=True)
708
    ipv4 = models.IPAddressField(null=False, unique=True, db_index=True)
709
    network = models.ForeignKey(Network, related_name="floating_ips",
710
                                null=False)
711
    machine = models.ForeignKey(VirtualMachine, related_name="floating_ips",
712
                                null=True)
713
    created = models.DateTimeField(auto_now_add=True)
714
    deleted = models.BooleanField(default=False, null=False)
715
    serial = models.ForeignKey(QuotaHolderSerial,
716
                               related_name="floating_ips", null=True)
717

    
718
    objects = ForUpdateManager()
719

    
720
    def __unicode__(self):
721
        return "<%s@%s>" % (self.ipv4, self.network.id)
722

    
723
    def in_use(self):
724
        if self.machine is None:
725
            return False
726
        else:
727
            return (not self.machine.deleted)
728

    
729

    
730
class PoolTable(models.Model):
731
    available_map = models.TextField(default="", null=False)
732
    reserved_map = models.TextField(default="", null=False)
733
    size = models.IntegerField(null=False)
734

    
735
    # Optional Fields
736
    base = models.CharField(null=True, max_length=32)
737
    offset = models.IntegerField(null=True)
738

    
739
    objects = ForUpdateManager()
740

    
741
    class Meta:
742
        abstract = True
743

    
744
    @classmethod
745
    def get_pool(cls):
746
        try:
747
            pool_row = cls.objects.select_for_update().get()
748
            return pool_row.pool
749
        except cls.DoesNotExist:
750
            raise pools.EmptyPool
751

    
752
    @property
753
    def pool(self):
754
        return self.manager(self)
755

    
756

    
757
class BridgePoolTable(PoolTable):
758
    manager = pools.BridgePool
759

    
760

    
761
class MacPrefixPoolTable(PoolTable):
762
    manager = pools.MacPrefixPool
763

    
764

    
765
class IPPoolTable(PoolTable):
766
    manager = pools.IPPool
767

    
768

    
769
@contextmanager
770
def pooled_rapi_client(obj):
771
        if isinstance(obj, VirtualMachine):
772
            backend = obj.backend
773
        else:
774
            backend = obj
775

    
776
        if backend.offline:
777
            log.warning("Trying to connect with offline backend: %s", backend)
778
            raise faults.ServiceUnavailable
779

    
780
        b = backend
781
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
782
                                 b.username, b.password)
783
        try:
784
            yield client
785
        finally:
786
            put_rapi_client(client)
787

    
788

    
789
class VirtualMachineDiagnosticManager(models.Manager):
790
    """
791
    Custom manager for :class:`VirtualMachineDiagnostic` model.
792
    """
793

    
794
    # diagnostic creation helpers
795
    def create_for_vm(self, vm, level, message, **kwargs):
796
        attrs = {'machine': vm, 'level': level, 'message': message}
797
        attrs.update(kwargs)
798
        # update instance updated time
799
        self.create(**attrs)
800
        vm.save()
801

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

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

    
808
    def since(self, vm, created_since, **kwargs):
809
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
810
                                           **kwargs)
811

    
812

    
813
class VirtualMachineDiagnostic(models.Model):
814
    """
815
    Model to store backend information messages that relate to the state of
816
    the virtual machine.
817
    """
818

    
819
    TYPES = (
820
        ('ERROR', 'Error'),
821
        ('WARNING', 'Warning'),
822
        ('INFO', 'Info'),
823
        ('DEBUG', 'Debug'),
824
    )
825

    
826
    objects = VirtualMachineDiagnosticManager()
827

    
828
    created = models.DateTimeField(auto_now_add=True)
829
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
830
    level = models.CharField(max_length=20, choices=TYPES)
831
    source = models.CharField(max_length=100)
832
    source_date = models.DateTimeField(null=True)
833
    message = models.CharField(max_length=255)
834
    details = models.TextField(null=True)
835

    
836
    class Meta:
837
        ordering = ['-created']