Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.4 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
    class Meta:
109
        verbose_name = u'Backend'
110
        ordering = ["clustername"]
111

    
112
    def __unicode__(self):
113
        return self.clustername + "(id=" + str(self.id) + ")"
114

    
115
    @property
116
    def backend_id(self):
117
        return self.id
118

    
119
    def get_client(self):
120
        """Get or create a client. """
121
        if self.offline:
122
            raise faults.ServiceUnavailable
123
        return get_rapi_client(self.id, self.hash,
124
                               self.clustername,
125
                               self.port,
126
                               self.username,
127
                               self.password)
128

    
129
    @staticmethod
130
    def put_client(client):
131
            put_rapi_client(client)
132

    
133
    def create_hash(self):
134
        """Create a hash for this backend. """
135
        sha = sha1('%s%s%s%s' %
136
                   (self.clustername, self.port, self.username, self.password))
137
        return sha.hexdigest()
138

    
139
    @property
140
    def password(self):
141
        return decrypt_db_charfield(self.password_hash)
142

    
143
    @password.setter
144
    def password(self, value):
145
        self.password_hash = encrypt_db_charfield(value)
146

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

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

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

    
186
    def use_hotplug(self):
187
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
188

    
189
    def get_create_params(self):
190
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
191
        params["hvparams"] = params.get("hvparams", {})\
192
                                   .get(self.hypervisor, {})
193
        return params
194

    
195

    
196
# A backend job may be in one of the following possible states
197
BACKEND_STATUSES = (
198
    ('queued', 'request queued'),
199
    ('waiting', 'request waiting for locks'),
200
    ('canceling', 'request being canceled'),
201
    ('running', 'request running'),
202
    ('canceled', 'request canceled'),
203
    ('success', 'request completed successfully'),
204
    ('error', 'request returned error')
205
)
206

    
207

    
208
class QuotaHolderSerial(models.Model):
209
    """Model representing a serial for a Quotaholder Commission.
210

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

218
    """
219
    serial = models.BigIntegerField(null=False, primary_key=True,
220
                                    db_index=True)
221
    pending = models.BooleanField(default=True, db_index=True)
222
    accept = models.BooleanField(default=False)
223
    resolved = models.BooleanField(default=False)
224

    
225
    class Meta:
226
        verbose_name = u'Quota Serial'
227
        ordering = ["serial"]
228

    
229

    
230
class VirtualMachine(models.Model):
231
    # The list of possible actions for a VM
232
    ACTIONS = (
233
        ('CREATE', 'Create VM'),
234
        ('START', 'Start VM'),
235
        ('STOP', 'Shutdown VM'),
236
        ('SUSPEND', 'Admin Suspend VM'),
237
        ('REBOOT', 'Reboot VM'),
238
        ('DESTROY', 'Destroy VM')
239
    )
240

    
241
    # The internal operating state of a VM
242
    OPER_STATES = (
243
        ('BUILD', 'Queued for creation'),
244
        ('ERROR', 'Creation failed'),
245
        ('STOPPED', 'Stopped'),
246
        ('STARTED', 'Started'),
247
        ('DESTROYED', 'Destroyed')
248
    )
249

    
250
    # The list of possible operations on the backend
251
    BACKEND_OPCODES = (
252
        ('OP_INSTANCE_CREATE', 'Create Instance'),
253
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
254
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
255
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
256
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
257

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

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

    
294
    # This dictionary contains the correspondence between
295
    # internal operating states and Server States as defined
296
    # by the Rackspace API.
297
    RSAPI_STATE_FROM_OPER_STATE = {
298
        "BUILD": "BUILD",
299
        "ERROR": "ERROR",
300
        "STOPPED": "STOPPED",
301
        "STARTED": "ACTIVE",
302
        "DESTROYED": "DELETED"
303
    }
304

    
305
    name = models.CharField('Virtual Machine Name', max_length=255)
306
    userid = models.CharField('User ID of the owner', max_length=100,
307
                              db_index=True, null=False)
308
    backend = models.ForeignKey(Backend, null=True,
309
                                related_name="virtual_machines",)
310
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
311
    created = models.DateTimeField(auto_now_add=True)
312
    updated = models.DateTimeField(auto_now=True)
313
    imageid = models.CharField(max_length=100, null=False)
314
    hostid = models.CharField(max_length=100)
315
    flavor = models.ForeignKey(Flavor)
316
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
317
    suspended = models.BooleanField('Administratively Suspended',
318
                                    default=False)
319
    serial = models.ForeignKey(QuotaHolderSerial,
320
                               related_name='virtual_machine', null=True)
321

    
322
    # VM State
323
    # The following fields are volatile data, in the sense
324
    # that they need not be persistent in the DB, but rather
325
    # get generated at runtime by quering Ganeti and applying
326
    # updates received from Ganeti.
327

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

    
342
    objects = ForUpdateManager()
343

    
344
    def get_client(self):
345
        if self.backend:
346
            return self.backend.get_client()
347
        else:
348
            raise faults.ServiceUnavailable
349

    
350
    def get_last_diagnostic(self, **filters):
351
        try:
352
            return self.diagnostics.filter()[0]
353
        except IndexError:
354
            return None
355

    
356
    @staticmethod
357
    def put_client(client):
358
            put_rapi_client(client)
359

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

    
373
    def save(self, *args, **kwargs):
374
        # Store hash for first time saved vm
375
        if (self.id is None or self.backend_hash == '') and self.backend:
376
            self.backend_hash = self.backend.hash
377
        super(VirtualMachine, self).save(*args, **kwargs)
378

    
379
    @property
380
    def backend_vm_id(self):
381
        """Returns the backend id for this VM by prepending backend-prefix."""
382
        if not self.id:
383
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
384
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
385

    
386
    class Meta:
387
        verbose_name = u'Virtual machine instance'
388
        get_latest_by = 'created'
389

    
390
    def __unicode__(self):
391
        return str(self.id)
392

    
393
    # Error classes
394
    class InvalidBackendIdError(Exception):
395
        def __init__(self, value):
396
            self.value = value
397

    
398
        def __str__(self):
399
            return repr(self.value)
400

    
401
    class InvalidBackendMsgError(Exception):
402
        def __init__(self, opcode, status):
403
            self.opcode = opcode
404
            self.status = status
405

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

    
410
    class InvalidActionError(Exception):
411
        def __init__(self, action):
412
            self._action = action
413

    
414
        def __str__(self):
415
            return repr(str(self._action))
416

    
417

    
418
class VirtualMachineMetadata(models.Model):
419
    meta_key = models.CharField(max_length=50)
420
    meta_value = models.CharField(max_length=500)
421
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
422

    
423
    class Meta:
424
        unique_together = (('meta_key', 'vm'),)
425
        verbose_name = u'Key-value pair of metadata for a VM.'
426

    
427
    def __unicode__(self):
428
        return u'%s: %s' % (self.meta_key, self.meta_value)
429

    
430

    
431
class Network(models.Model):
432
    OPER_STATES = (
433
        ('PENDING', 'Pending'),  # Unused because of lazy networks
434
        ('ACTIVE', 'Active'),
435
        ('DELETED', 'Deleted'),
436
        ('ERROR', 'Error')
437
    )
438

    
439
    ACTIONS = (
440
        ('CREATE', 'Create Network'),
441
        ('DESTROY', 'Destroy Network'),
442
    )
443

    
444
    RSAPI_STATE_FROM_OPER_STATE = {
445
        'PENDING': 'PENDING',
446
        'ACTIVE': 'ACTIVE',
447
        'DELETED': 'DELETED',
448
        'ERROR': 'ERROR'
449
    }
450

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

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

    
510
    pool = models.OneToOneField('IPPoolTable', related_name='network',
511
                default=lambda: IPPoolTable.objects.create(available_map='',
512
                                                           reserved_map='',
513
                                                           size=0),
514
                null=True)
515
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
516
                               null=True)
517

    
518
    objects = ForUpdateManager()
519

    
520
    def __unicode__(self):
521
        return str(self.id)
522

    
523
    @property
524
    def backend_id(self):
525
        """Return the backend id by prepending backend-prefix."""
526
        if not self.id:
527
            raise Network.InvalidBackendIdError("self.id is None")
528
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
529

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

534
        """
535
        if self.tags:
536
            return self.tags.split(',')
537
        else:
538
            return []
539

    
540
    def create_backend_network(self, backend=None):
541
        """Create corresponding BackendNetwork entries."""
542

    
543
        backends = [backend] if backend\
544
                             else Backend.objects.filter(offline=False)
545
        for backend in backends:
546
            backend_exists =\
547
                BackendNetwork.objects.filter(backend=backend, network=self)\
548
                                      .exists()
549
            if not backend_exists:
550
                BackendNetwork.objects.create(backend=backend, network=self)
551

    
552
    def get_pool(self):
553
        if not self.pool_id:
554
            self.pool = IPPoolTable.objects.create(available_map='',
555
                                                   reserved_map='',
556
                                                   size=0)
557
            self.save()
558
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
559
                                                      .pool
560

    
561
    def reserve_address(self, address):
562
        pool = self.get_pool()
563
        pool.reserve(address)
564
        pool.save()
565

    
566
    def release_address(self, address):
567
        pool = self.get_pool()
568
        pool.put(address)
569
        pool.save()
570

    
571
    class InvalidBackendIdError(Exception):
572
        def __init__(self, value):
573
            self.value = value
574

    
575
        def __str__(self):
576
            return repr(self.value)
577

    
578
    class InvalidBackendMsgError(Exception):
579
        def __init__(self, opcode, status):
580
            self.opcode = opcode
581
            self.status = status
582

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

    
587
    class InvalidActionError(Exception):
588
        def __init__(self, action):
589
            self._action = action
590

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

    
594

    
595
class BackendNetwork(models.Model):
596
    OPER_STATES = (
597
        ('PENDING', 'Pending'),
598
        ('ACTIVE', 'Active'),
599
        ('DELETED', 'Deleted'),
600
        ('ERROR', 'Error')
601
    )
602

    
603
    # The list of possible operations on the backend
604
    BACKEND_OPCODES = (
605
        ('OP_NETWORK_ADD', 'Create Network'),
606
        ('OP_NETWORK_CONNECT', 'Activate Network'),
607
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
608
        ('OP_NETWORK_REMOVE', 'Remove Network'),
609
        # These are listed here for completeness,
610
        # and are ignored for the time being
611
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
612
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
613
    )
614

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

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

    
645
    class Meta:
646
        # Ensure one entry for each network in each backend
647
        unique_together = (("network", "backend"))
648

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

    
665
    def __unicode__(self):
666
        return '<%s@%s>' % (self.network, self.backend)
667

    
668

    
669
class NetworkInterface(models.Model):
670
    FIREWALL_PROFILES = (
671
        ('ENABLED', 'Enabled'),
672
        ('DISABLED', 'Disabled'),
673
        ('PROTECTED', 'Protected')
674
    )
675

    
676
    STATES = (
677
        ("ACTIVE", "Active"),
678
        ("BUILDING", "Building"),
679
    )
680

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

    
695
    def __unicode__(self):
696
        return '%s@%s' % (self.machine.name, self.network.name)
697

    
698

    
699
class PoolTable(models.Model):
700
    available_map = models.TextField(default="", null=False)
701
    reserved_map = models.TextField(default="", null=False)
702
    size = models.IntegerField(null=False)
703

    
704
    # Optional Fields
705
    base = models.CharField(null=True, max_length=32)
706
    offset = models.IntegerField(null=True)
707

    
708
    objects = ForUpdateManager()
709

    
710
    class Meta:
711
        abstract = True
712

    
713
    @classmethod
714
    def get_pool(cls):
715
        try:
716
            pool_row = cls.objects.select_for_update().get()
717
            return pool_row.pool
718
        except cls.DoesNotExist:
719
            raise pools.EmptyPool
720

    
721
    @property
722
    def pool(self):
723
        return self.manager(self)
724

    
725

    
726
class BridgePoolTable(PoolTable):
727
    manager = pools.BridgePool
728

    
729

    
730
class MacPrefixPoolTable(PoolTable):
731
    manager = pools.MacPrefixPool
732

    
733

    
734
class IPPoolTable(PoolTable):
735
    manager = pools.IPPool
736

    
737

    
738
@contextmanager
739
def pooled_rapi_client(obj):
740
        if isinstance(obj, VirtualMachine):
741
            backend = obj.backend
742
        else:
743
            backend = obj
744

    
745
        if backend.offline:
746
            log.warning("Trying to connect with offline backend: %s", backend)
747
            raise faults.ServiceUnavailable
748

    
749
        b = backend
750
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
751
                                 b.username, b.password)
752
        try:
753
            yield client
754
        finally:
755
            put_rapi_client(client)
756

    
757

    
758
class VirtualMachineDiagnosticManager(models.Manager):
759
    """
760
    Custom manager for :class:`VirtualMachineDiagnostic` model.
761
    """
762

    
763
    # diagnostic creation helpers
764
    def create_for_vm(self, vm, level, message, **kwargs):
765
        attrs = {'machine': vm, 'level': level, 'message': message}
766
        attrs.update(kwargs)
767
        # update instance updated time
768
        self.create(**attrs)
769
        vm.save()
770

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

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

    
777
    def since(self, vm, created_since, **kwargs):
778
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
779
                                           **kwargs)
780

    
781

    
782
class VirtualMachineDiagnostic(models.Model):
783
    """
784
    Model to store backend information messages that relate to the state of
785
    the virtual machine.
786
    """
787

    
788
    TYPES = (
789
        ('ERROR', 'Error'),
790
        ('WARNING', 'Warning'),
791
        ('INFO', 'Info'),
792
        ('DEBUG', 'Debug'),
793
    )
794

    
795
    objects = VirtualMachineDiagnosticManager()
796

    
797
    created = models.DateTimeField(auto_now_add=True)
798
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
799
    level = models.CharField(max_length=20, choices=TYPES)
800
    source = models.CharField(max_length=100)
801
    source_date = models.DateTimeField(null=True)
802
    message = models.CharField(max_length=255)
803
    details = models.TextField(null=True)
804

    
805
    class Meta:
806
        ordering = ['-created']